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 ResizeNearest2x {
723 src: usize,
724 dst: usize,
725 n: u32,
726 c: u32,
727 h: u32,
728 w: u32,
729 },
730 AxialRope2d {
732 src: usize,
733 dst: usize,
734 batch: u32,
735 seq: u32,
736 hidden: u32,
737 end_x: u32,
738 end_y: u32,
739 head_dim: u32,
740 num_heads: u32,
741 theta: f32,
742 repeat_factor: u32,
743 },
744 RmsNorm {
747 src: usize,
748 g: usize,
749 b: usize,
750 dst: usize,
751 rows: u32,
752 h: u32,
753 eps: f32,
754 },
755 Softmax {
757 data: usize,
758 rows: u32,
759 cols: u32,
760 },
761 Cumsum {
764 src: usize,
765 dst: usize,
766 rows: u32,
767 cols: u32,
768 exclusive: bool,
769 },
770 SelectiveScan {
774 x: usize,
775 delta: usize,
776 a: usize,
777 b: usize,
778 c: usize,
779 dst: usize,
780 batch: u32,
781 seq: u32,
782 hidden: u32,
783 state_size: u32,
784 },
785
786 GatedDeltaNet {
790 q: usize,
791 k: usize,
792 v: usize,
793 g: usize,
794 beta: usize,
795 state: usize,
798 dst: usize,
799 batch: u32,
800 seq: u32,
801 heads: u32,
802 state_size: u32,
803 },
804
805 Lstm {
809 x: usize,
810 w_ih: usize,
811 w_hh: usize,
812 bias: usize,
813 h0: usize,
814 c0: usize,
815 dst: usize,
816 batch: u32,
817 seq: u32,
818 input_size: u32,
819 hidden: u32,
820 num_layers: u32,
821 bidirectional: bool,
822 carry: bool,
823 },
824 Gru {
826 x: usize,
827 w_ih: usize,
828 w_hh: usize,
829 b_ih: usize,
830 b_hh: usize,
831 h0: usize,
832 dst: usize,
833 batch: u32,
834 seq: u32,
835 input_size: u32,
836 hidden: u32,
837 num_layers: u32,
838 bidirectional: bool,
839 carry: bool,
840 },
841 Rnn {
843 x: usize,
844 w_ih: usize,
845 w_hh: usize,
846 bias: usize,
847 h0: usize,
848 dst: usize,
849 batch: u32,
850 seq: u32,
851 input_size: u32,
852 hidden: u32,
853 num_layers: u32,
854 bidirectional: bool,
855 carry: bool,
856 relu: bool,
857 },
858 Mamba2 {
860 x: usize,
861 dt: usize,
862 a: usize,
863 b: usize,
864 c: usize,
865 dst: usize,
866 batch: u32,
867 seq: u32,
868 heads: u32,
869 head_dim: u32,
870 state_size: u32,
871 },
872
873 Conv2D1x1 {
883 src: usize,
884 weight: usize,
885 dst: usize,
886 n: u32,
887 c_in: u32,
888 c_out: u32,
889 hw: u32,
890 },
891
892 DequantMatMul {
896 x: usize,
897 w_q: usize, scale: usize, zp: usize, dst: usize,
901 m: u32,
902 k: u32,
903 n: u32,
904 block_size: u32,
905 is_asymmetric: bool,
906 },
907
908 DequantMatMulGguf {
918 x: usize, w_q: usize, dst: usize, m: u32,
922 k: u32,
923 n: u32,
924 scheme: rlx_ir::quant::QuantScheme,
925 },
926
927 DequantMatMulInt4 {
929 x: usize,
930 w_q: usize,
931 scale: usize,
932 zp: usize,
933 dst: usize,
934 m: u32,
935 k: u32,
936 n: u32,
937 block_size: u32,
938 is_asymmetric: bool,
939 },
940
941 DequantMatMulFp8 {
943 x: usize,
944 w_q: usize,
945 scale: usize,
946 dst: usize,
947 m: u32,
948 k: u32,
949 n: u32,
950 e5m2: bool,
951 },
952
953 DequantMatMulNvfp4 {
955 x: usize,
956 w_q: usize,
957 scale: usize,
958 global_scale: usize,
959 dst: usize,
960 m: u32,
961 k: u32,
962 n: u32,
963 },
964
965 ScaledMatMul {
968 lhs: usize,
969 rhs: usize,
970 lhs_scale: usize,
971 rhs_scale: usize,
972 bias: usize, dst: usize,
974 m: u32,
975 k: u32,
976 n: u32,
977 lhs_fmt: rlx_ir::ScaledFormat,
978 rhs_fmt: rlx_ir::ScaledFormat,
979 layout: rlx_ir::ScaleLayout,
980 has_bias: bool,
981 },
982
983 ScaledQuantize {
985 x: usize,
986 scale: usize,
987 dst: usize,
988 rows: u32,
989 cols: u32,
990 fmt: rlx_ir::ScaledFormat,
991 layout: rlx_ir::ScaleLayout,
992 },
993
994 ScaledQuantScale {
996 x: usize,
997 dst: usize,
998 rows: u32,
999 cols: u32,
1000 fmt: rlx_ir::ScaledFormat,
1001 layout: rlx_ir::ScaleLayout,
1002 },
1003
1004 ScaledDequantize {
1006 codes: usize,
1007 scale: usize,
1008 dst: usize,
1009 rows: u32,
1010 cols: u32,
1011 fmt: rlx_ir::ScaledFormat,
1012 layout: rlx_ir::ScaleLayout,
1013 },
1014
1015 LoraMatMul {
1019 x: usize,
1020 w: usize,
1021 a: usize,
1022 b: usize,
1023 dst: usize,
1024 m: u32,
1025 k: u32,
1026 n: u32,
1027 r: u32,
1028 scale: f32,
1029 },
1030 Sample {
1034 logits: usize,
1035 dst: usize,
1036 batch: u32,
1037 vocab: u32,
1038 top_k: u32, top_p: f32, temperature: f32, seed: u64,
1042 },
1043 RngNormal {
1045 dst: usize,
1046 len: u32,
1047 mean: f32,
1048 scale: f32,
1049 key: u64,
1050 op_seed: Option<f32>,
1051 },
1052 RngUniform {
1054 dst: usize,
1055 len: u32,
1056 low: f32,
1057 high: f32,
1058 key: u64,
1059 op_seed: Option<f32>,
1060 },
1061 Attention {
1072 q: usize,
1073 k: usize,
1074 v: usize,
1075 mask: usize,
1076 out: usize,
1077 batch: u32,
1078 seq: u32,
1080 kv_seq: u32,
1082 heads: u32,
1083 head_dim: u32,
1084 mask_kind: rlx_ir::op::MaskKind,
1085 scale: f32,
1089 q_row_stride: u32,
1090 k_row_stride: u32,
1091 v_row_stride: u32,
1092 bhsd: bool,
1100 },
1101 AttentionBackward {
1103 q: usize,
1104 k: usize,
1105 v: usize,
1106 dy: usize,
1107 mask: usize,
1108 out: usize,
1109 batch: u32,
1110 seq: u32,
1111 kv_seq: u32,
1112 heads: u32,
1113 head_dim: u32,
1114 mask_kind: rlx_ir::op::MaskKind,
1115 wrt: rlx_ir::op::AttentionBwdWrt,
1116 bhsd: bool,
1117 },
1118 Rope {
1124 src: usize,
1125 cos: usize,
1126 sin: usize,
1127 dst: usize,
1128 batch: u32,
1129 seq: u32,
1130 hidden: u32,
1131 head_dim: u32,
1132 n_rot: u32,
1133 cos_len: u32,
1134 src_row_stride: u32,
1135 interleaved: bool,
1138 },
1139 FusedAttnBlock {
1142 hidden: usize,
1143 qkv_w: usize,
1144 out_w: usize,
1145 mask: usize,
1146 mask_kind: rlx_ir::op::MaskKind,
1154 out: usize,
1155 qkv_b: usize,
1156 out_b: usize, cos: usize,
1158 sin: usize,
1159 cos_len: u32, batch: u32,
1161 seq: u32,
1162 hs: u32,
1163 nh: u32,
1164 dh: u32,
1165 has_bias: bool,
1166 has_rope: bool,
1167 interleaved: bool,
1172 },
1173 FusedBertLayer {
1176 hidden: usize,
1178 qkv_w: usize,
1179 qkv_b: usize,
1180 out_w: usize,
1181 out_b: usize,
1182 mask: usize,
1183 ln1_g: usize,
1185 ln1_b: usize,
1186 eps1: f32,
1187 fc1_w: usize,
1189 fc1_b: usize,
1190 fc2_w: usize,
1191 fc2_b: usize,
1192 ln2_g: usize,
1194 ln2_b: usize,
1195 eps2: f32,
1196 out: usize,
1198 batch: u32,
1200 seq: u32,
1201 hs: u32,
1202 nh: u32,
1203 dh: u32,
1204 int_dim: u32,
1205 },
1206 FusedNomicLayer {
1208 hidden: usize,
1209 qkv_w: usize,
1210 out_w: usize,
1211 mask: usize,
1212 cos: usize,
1213 sin: usize,
1214 cos_len: u32,
1215 ln1_g: usize,
1216 ln1_b: usize,
1217 eps1: f32,
1218 fc11_w: usize,
1219 fc12_w: usize,
1220 fc2_w: usize,
1221 ln2_g: usize,
1222 ln2_b: usize,
1223 eps2: f32,
1224 out: usize,
1225 batch: u32,
1226 seq: u32,
1227 hs: u32,
1228 nh: u32,
1229 dh: u32,
1230 int_dim: u32,
1231 interleaved: bool,
1234 },
1235 FusedSwiGLU {
1239 src: usize,
1240 dst: usize,
1241 n_half: u32,
1242 total: u32,
1243 gate_first: bool,
1244 },
1245 Concat {
1250 dst: usize,
1251 outer: u32,
1252 inner: u32,
1253 total_axis: u32,
1254 inputs: Vec<(usize, u32, u32)>,
1257 },
1258 Compare {
1260 lhs: usize,
1261 rhs: usize,
1262 dst: usize,
1263 len: u32,
1264 op: CmpOp,
1265 inputs_i64: u8,
1267 inputs_elem_bytes: u8,
1269 dst_elem_bytes: u8,
1271 },
1272 Reduce {
1280 src: usize,
1281 dst: usize,
1282 outer: u32,
1283 reduced: u32,
1284 inner: u32,
1285 op: ReduceOp,
1286 },
1287 ArgReduce {
1290 src: usize,
1291 dst: usize,
1292 outer: u32,
1293 reduced: u32,
1294 inner: u32,
1295 is_max: bool,
1296 },
1297 TopK {
1301 src: usize,
1302 dst: usize,
1303 outer: u32,
1304 axis_dim: u32,
1305 k: u32,
1306 indices_i64: u8,
1307 },
1308 GroupedMatMul {
1312 input: usize,
1313 weight: usize,
1314 expert_idx: usize,
1315 dst: usize,
1316 m: u32,
1317 k_dim: u32,
1318 n: u32,
1319 num_experts: u32,
1320 },
1321 DequantGroupedMatMulGguf {
1323 input: usize,
1324 w_q: usize,
1325 expert_idx: usize,
1326 dst: usize,
1327 m: u32,
1328 k_dim: u32,
1329 n: u32,
1330 num_experts: u32,
1331 scheme: rlx_ir::quant::QuantScheme,
1332 },
1333 DequantMoEWeightsGguf {
1335 w_q: usize,
1336 dst: usize,
1337 k_dim: u32,
1338 n: u32,
1339 num_experts: u32,
1340 scheme: rlx_ir::quant::QuantScheme,
1341 },
1342 ScatterAdd {
1345 updates: usize,
1346 indices: usize,
1347 dst: usize,
1348 num_updates: u32,
1349 out_dim: u32,
1350 trailing: u32,
1351 },
1352 Where {
1354 cond: usize,
1355 on_true: usize,
1356 on_false: usize,
1357 dst: usize,
1358 len: u32,
1359 elem_bytes: u8,
1360 cond_elem_bytes: u8,
1362 },
1363 Fma {
1366 a: usize,
1367 b: usize,
1368 c: usize,
1369 dst: usize,
1370 len: u32,
1371 elem_bytes: u8,
1372 },
1373 Transpose {
1379 src: usize,
1380 dst: usize,
1381 in_total: u32,
1382 out_dims: Vec<u32>,
1383 in_strides: Vec<u32>,
1384 elem_bytes: u8,
1385 },
1386 GatherAxis {
1391 table: usize,
1392 idx: usize,
1393 dst: usize,
1394 outer: u32,
1395 axis_dim: u32,
1396 num_idx: u32,
1397 trailing: u32,
1398 idx_i64: u8,
1399 table_bytes: u8,
1400 },
1401 Pool2D {
1405 src: usize,
1406 dst: usize,
1407 n: u32,
1408 c: u32,
1409 h: u32,
1410 w: u32,
1411 h_out: u32,
1412 w_out: u32,
1413 kh: u32,
1414 kw: u32,
1415 sh: u32,
1416 sw: u32,
1417 ph: u32,
1418 pw: u32,
1419 kind: ReduceOp,
1420 },
1421 Conv2D {
1426 src: usize,
1427 weight: usize,
1428 dst: usize,
1429 n: u32,
1430 c_in: u32,
1431 h: u32,
1432 w: u32,
1433 c_out: u32,
1434 h_out: u32,
1435 w_out: u32,
1436 kh: u32,
1437 kw: u32,
1438 sh: u32,
1439 sw: u32,
1440 ph: u32,
1441 pw: u32,
1442 dh: u32,
1443 dw: u32,
1444 groups: u32,
1445 },
1446
1447 QMatMul {
1455 x: usize,
1456 w: usize,
1457 bias: usize,
1458 out: usize,
1459 m: u32,
1460 k: u32,
1461 n: u32,
1462 x_zp: i32,
1463 w_zp: i32,
1464 out_zp: i32,
1465 mult: f32,
1466 },
1467
1468 QConv2d {
1472 x: usize,
1473 w: usize,
1474 bias: usize,
1475 out: usize,
1476 n: u32,
1477 c_in: u32,
1478 h: u32,
1479 w_in: u32,
1480 c_out: u32,
1481 h_out: u32,
1482 w_out: u32,
1483 kh: u32,
1484 kw: u32,
1485 sh: u32,
1486 sw: u32,
1487 ph: u32,
1488 pw: u32,
1489 dh: u32,
1490 dw: u32,
1491 groups: u32,
1492 x_zp: i32,
1493 w_zp: i32,
1494 out_zp: i32,
1495 mult: f32,
1496 },
1497
1498 Quantize {
1505 x: usize,
1506 q: usize,
1507 len: u32,
1508 chan_axis: u32,
1509 chan_dim: u32,
1510 inner: u32,
1511 scales: Vec<f32>,
1512 zero_points: Vec<i32>,
1513 },
1514
1515 Dequantize {
1517 q: usize,
1518 x: usize,
1519 len: u32,
1520 chan_axis: u32,
1521 chan_dim: u32,
1522 inner: u32,
1523 scales: Vec<f32>,
1524 zero_points: Vec<i32>,
1525 },
1526
1527 FakeQuantize {
1538 x: usize,
1539 out: usize,
1540 len: u32,
1541 chan_axis: u32,
1542 chan_dim: u32,
1543 inner: u32,
1544 bits: u8,
1545 ste: rlx_ir::op::SteKind,
1549 scale_mode: rlx_ir::op::ScaleMode,
1554 state_off: Option<usize>,
1558 },
1559
1560 FakeQuantizeBackward {
1565 x: usize,
1566 dy: usize,
1567 dx: usize,
1568 len: u32,
1569 chan_axis: u32,
1570 chan_dim: u32,
1571 inner: u32,
1572 bits: u8,
1573 ste: rlx_ir::op::SteKind,
1574 },
1575
1576 FakeQuantizeLSQ {
1579 x: usize,
1580 scale_off: usize,
1581 out: usize,
1582 len: u32,
1583 chan_axis: u32,
1584 chan_dim: u32,
1585 inner: u32,
1586 bits: u8,
1587 },
1588
1589 FakeQuantizeLSQBackwardX {
1592 x: usize,
1593 scale_off: usize,
1594 dy: usize,
1595 dx: usize,
1596 len: u32,
1597 chan_axis: u32,
1598 chan_dim: u32,
1599 inner: u32,
1600 bits: u8,
1601 },
1602
1603 FakeQuantizeLSQBackwardScale {
1608 x: usize,
1609 scale_off: usize,
1610 dy: usize,
1611 dscale: usize,
1612 len: u32,
1613 chan_axis: u32,
1614 chan_dim: u32,
1615 inner: u32,
1616 bits: u8,
1617 },
1618
1619 ReluBackward {
1621 x: usize,
1622 dy: usize,
1623 dx: usize,
1624 len: u32,
1625 },
1626 ReluBackwardF64 {
1632 x: usize,
1633 dy: usize,
1634 dx: usize,
1635 len: u32,
1636 },
1637
1638 ActivationBackward {
1643 x: usize,
1644 dy: usize,
1645 dx: usize,
1646 len: u32,
1647 kind: Activation,
1648 },
1649 ActivationBackwardF64 {
1655 x: usize,
1656 dy: usize,
1657 dx: usize,
1658 len: u32,
1659 kind: Activation,
1660 },
1661
1662 LayerNormBackwardInput {
1665 x: usize,
1666 gamma: usize,
1667 dy: usize,
1668 dx: usize,
1669 rows: u32,
1670 h: u32,
1671 eps: f32,
1672 },
1673
1674 LayerNormBackwardGamma {
1676 x: usize,
1677 dy: usize,
1678 dgamma: usize,
1679 rows: u32,
1680 h: u32,
1681 eps: f32,
1682 },
1683
1684 RmsNormBackwardInput {
1685 x: usize,
1686 gamma: usize,
1687 beta: usize,
1688 dy: usize,
1689 dx: usize,
1690 rows: u32,
1691 h: u32,
1692 eps: f32,
1693 },
1694 RmsNormBackwardGamma {
1695 x: usize,
1696 gamma: usize,
1697 beta: usize,
1698 dy: usize,
1699 dgamma: usize,
1700 rows: u32,
1701 h: u32,
1702 eps: f32,
1703 },
1704 RmsNormBackwardBeta {
1705 x: usize,
1706 gamma: usize,
1707 beta: usize,
1708 dy: usize,
1709 dbeta: usize,
1710 rows: u32,
1711 h: u32,
1712 eps: f32,
1713 },
1714 RopeBackward {
1715 dy: usize,
1716 cos: usize,
1717 sin: usize,
1718 dx: usize,
1719 batch: u32,
1720 seq: u32,
1721 hidden: u32,
1722 head_dim: u32,
1723 n_rot: u32,
1724 cos_len: u32,
1725 },
1726 CumsumBackward {
1727 dy: usize,
1728 dx: usize,
1729 rows: u32,
1730 cols: u32,
1731 exclusive: bool,
1732 },
1733 GatherBackward {
1734 dy: usize,
1735 indices: usize,
1736 dst: usize,
1737 outer: u32,
1738 axis_dim: u32,
1739 num_idx: u32,
1740 trailing: u32,
1741 },
1742
1743 GroupNormBackwardInput {
1744 x: usize,
1745 gamma: usize,
1746 beta: usize,
1747 dy: usize,
1748 dx: usize,
1749 n: u32,
1750 c: u32,
1751 h: u32,
1752 w: u32,
1753 num_groups: u32,
1754 eps: f32,
1755 },
1756 GroupNormBackwardGamma {
1757 x: usize,
1758 dy: usize,
1759 dgamma: usize,
1760 n: u32,
1761 c: u32,
1762 h: u32,
1763 w: u32,
1764 num_groups: u32,
1765 eps: f32,
1766 },
1767 GroupNormBackwardBeta {
1768 dy: usize,
1769 dbeta: usize,
1770 n: u32,
1771 c: u32,
1772 h: u32,
1773 w: u32,
1774 },
1775
1776 MaxPool2dBackward {
1782 x: usize,
1783 dy: usize,
1784 dx: usize,
1785 n: u32,
1786 c: u32,
1787 h: u32,
1788 w: u32,
1789 h_out: u32,
1790 w_out: u32,
1791 kh: u32,
1792 kw: u32,
1793 sh: u32,
1794 sw: u32,
1795 ph: u32,
1796 pw: u32,
1797 },
1798
1799 Conv2dBackwardInput {
1803 dy: usize,
1804 w: usize,
1805 dx: usize,
1806 n: u32,
1807 c_in: u32,
1808 h: u32,
1809 w_in: u32,
1810 c_out: u32,
1811 h_out: u32,
1812 w_out: u32,
1813 kh: u32,
1814 kw: u32,
1815 sh: u32,
1816 sw: u32,
1817 ph: u32,
1818 pw: u32,
1819 dh: u32,
1820 dw: u32,
1821 groups: u32,
1822 },
1823
1824 Conv2dBackwardWeight {
1828 x: usize,
1829 dy: usize,
1830 dw: usize,
1831 n: u32,
1832 c_in: u32,
1833 h: u32,
1834 w: u32,
1835 c_out: u32,
1836 h_out: u32,
1837 w_out: u32,
1838 kh: u32,
1839 kw: u32,
1840 sh: u32,
1841 sw: u32,
1842 ph: u32,
1843 pw: u32,
1844 dh: u32,
1845 dw_dil: u32,
1846 groups: u32,
1847 },
1848
1849 Im2Col {
1852 x: usize,
1853 col: usize,
1854 n: u32,
1855 c_in: u32,
1856 h: u32,
1857 w: u32,
1858 h_out: u32,
1859 w_out: u32,
1860 kh: u32,
1861 kw: u32,
1862 sh: u32,
1863 sw: u32,
1864 ph: u32,
1865 pw: u32,
1866 dh: u32,
1867 dw_dil: u32,
1868 },
1869
1870 SoftmaxCrossEntropyDense {
1875 logits: usize,
1876 targets: usize,
1877 dst: usize,
1878 n: u32,
1879 c: u32,
1880 },
1881
1882 SoftmaxCrossEntropy {
1886 logits: usize,
1887 labels: usize,
1888 dst: usize,
1889 n: u32,
1890 c: u32,
1891 },
1892
1893 SoftmaxCrossEntropyBackward {
1896 logits: usize,
1897 labels: usize,
1898 d_loss: usize,
1899 dlogits: usize,
1900 n: u32,
1901 c: u32,
1902 },
1903
1904 CustomOp {
1910 kernel: Arc<dyn CpuKernel>,
1911 inputs: Vec<(usize, u32, Shape)>, output: (usize, u32, Shape), attrs: Vec<u8>,
1914 },
1915
1916 GaussianSplatRender {
1926 positions_off: usize,
1927 positions_len: usize,
1928 scales_off: usize,
1929 scales_len: usize,
1930 rotations_off: usize,
1931 rotations_len: usize,
1932 opacities_off: usize,
1933 opacities_len: usize,
1934 colors_off: usize,
1935 colors_len: usize,
1936 sh_coeffs_off: usize,
1937 sh_coeffs_len: usize,
1938 meta_off: usize,
1939 dst_off: usize,
1940 dst_len: usize,
1941 width: u32,
1942 height: u32,
1943 tile_size: u32,
1944 radius_scale: f32,
1945 alpha_cutoff: f32,
1946 max_splat_steps: u32,
1947 transmittance_threshold: f32,
1948 max_list_entries: u32,
1949 },
1950 GaussianSplatRenderBackward {
1951 positions_off: usize,
1952 positions_len: usize,
1953 scales_off: usize,
1954 scales_len: usize,
1955 rotations_off: usize,
1956 rotations_len: usize,
1957 opacities_off: usize,
1958 opacities_len: usize,
1959 colors_off: usize,
1960 colors_len: usize,
1961 sh_coeffs_off: usize,
1962 sh_coeffs_len: usize,
1963 meta_off: usize,
1964 d_loss_off: usize,
1965 d_loss_len: usize,
1966 packed_off: usize,
1967 packed_len: usize,
1968 width: u32,
1969 height: u32,
1970 tile_size: u32,
1971 radius_scale: f32,
1972 alpha_cutoff: f32,
1973 max_splat_steps: u32,
1974 transmittance_threshold: f32,
1975 max_list_entries: u32,
1976 loss_grad_clip: f32,
1977 sh_band: u32,
1978 max_anisotropy: f32,
1979 },
1980 GaussianSplatPrepare {
1982 positions_off: usize,
1983 positions_len: usize,
1984 scales_off: usize,
1985 scales_len: usize,
1986 rotations_off: usize,
1987 rotations_len: usize,
1988 opacities_off: usize,
1989 opacities_len: usize,
1990 colors_off: usize,
1991 colors_len: usize,
1992 sh_coeffs_off: usize,
1993 sh_coeffs_len: usize,
1994 meta_off: usize,
1995 meta_len: usize,
1996 prep_off: usize,
1997 prep_len: usize,
1998 width: u32,
1999 height: u32,
2000 tile_size: u32,
2001 radius_scale: f32,
2002 alpha_cutoff: f32,
2003 max_splat_steps: u32,
2004 transmittance_threshold: f32,
2005 max_list_entries: u32,
2006 },
2007 GaussianSplatRasterize {
2009 prep_off: usize,
2010 prep_len: usize,
2011 meta_off: usize,
2012 meta_len: usize,
2013 dst_off: usize,
2014 dst_len: usize,
2015 count: usize,
2016 width: u32,
2017 height: u32,
2018 tile_size: u32,
2019 alpha_cutoff: f32,
2020 max_splat_steps: u32,
2021 transmittance_threshold: f32,
2022 max_list_entries: u32,
2023 },
2024 Fft1d {
2025 src: usize,
2026 dst: usize,
2027 outer: u32,
2028 n_complex: u32,
2029 inverse: bool,
2030 norm_tag: u32,
2031 dtype: rlx_ir::DType,
2032 },
2033 FftButterflyStage {
2034 state_src: usize,
2035 state_dst: usize,
2036 gate_src: usize,
2037 rev_src: usize,
2038 tw_re_src: usize,
2039 tw_im_src: usize,
2040 batch: u32,
2041 n_fft: u32,
2042 stage: u32,
2043 },
2044 LogMel {
2045 spec: usize,
2046 filters: usize,
2047 dst: usize,
2048 outer: u32,
2049 n_fft: u32,
2050 n_bins: u32,
2051 n_mels: u32,
2052 },
2053 LogMelBackward {
2054 spec: usize,
2055 filters: usize,
2056 dy: usize,
2057 dst: usize,
2058 outer: u32,
2059 n_fft: u32,
2060 n_bins: u32,
2061 n_mels: u32,
2062 },
2063 WelchPeaks {
2064 spec: usize,
2065 dst: usize,
2066 welch_batch: u32,
2067 n_fft: u32,
2068 n_segments: u32,
2069 k: u32,
2070 },
2071}
2072
2073#[derive(Clone)]
2076pub struct ThunkSchedule {
2077 pub thunks: Vec<Thunk>,
2078 pub moe_resident: Option<std::sync::Arc<[bool]>>,
2080 pub moe_resident_layers: Option<std::sync::Arc<Vec<std::sync::Arc<[bool]>>>>,
2082 pub moe_topk_capture: Option<std::sync::Arc<crate::moe_topk_capture::MoeTopkCapture>>,
2084 pub mask_threshold: f32,
2086 pub mask_neg_inf: f32,
2087 pub score_skip: f32,
2088 pub compiled_fns: Vec<Arc<dyn Fn(*mut u8) + Send + Sync>>,
2094 pub rng: Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
2096}
2097
2098impl ThunkSchedule {
2099 pub fn strip_nops(&mut self) {
2100 self.thunks.retain(|t| !matches!(t, Thunk::Nop));
2101 self.compiled_fns.clear();
2104 }
2105}
2106
2107fn node_offset(arena: &Arena, id: NodeId) -> usize {
2109 if arena.has_buffer(id) {
2110 arena.byte_offset(id)
2111 } else {
2112 usize::MAX
2113 }
2114}
2115
2116fn thunk_read_offsets(t: &Thunk) -> Vec<usize> {
2122 match t {
2123 Thunk::Sgemm { a, b, .. } => vec![*a, *b],
2124 Thunk::SgemmT { a, b, .. } => vec![*a, *b],
2125 Thunk::SgdMomentum {
2126 param, vel, grad, ..
2127 } => vec![*param, *vel, *grad],
2128 Thunk::DenseSolveF64 { a, b, .. } => vec![*a, *b],
2129 Thunk::DenseSolveF32 { a, b, .. } => vec![*a, *b],
2130 Thunk::BatchedDenseSolveF64 { a, b, .. } => vec![*a, *b],
2131 Thunk::BatchedDgemmF64 { a, b, .. } => vec![*a, *b],
2132 Thunk::BatchedSgemm { a, b, .. } => vec![*a, *b],
2133 Thunk::FusedMmBiasAct { a, w, bias, .. } => vec![*a, *w, *bias],
2134 Thunk::ElementwiseRegion { input_offs, .. } => input_offs.clone(),
2135 Thunk::BiasAdd { src, bias, .. } => vec![*src, *bias],
2136 Thunk::BinaryFull { lhs, rhs, .. } => vec![*lhs, *rhs],
2137 Thunk::BinaryFullF64 { lhs, rhs, .. } => vec![*lhs, *rhs],
2138 Thunk::BinaryFullC64 { lhs, rhs, .. } => vec![*lhs, *rhs],
2139 Thunk::ComplexNormSqF32 { src, .. } => vec![*src],
2140 Thunk::ComplexNormSqBackwardF32 { z, g, .. } => vec![*z, *g],
2141 Thunk::ConjugateC64 { src, .. } => vec![*src],
2142 Thunk::Scan {
2143 outer_init_off,
2144 xs_inputs,
2145 ..
2146 } => {
2147 let mut v = vec![*outer_init_off];
2148 for (_, outer_xs_off, _) in xs_inputs.iter() {
2149 v.push(*outer_xs_off);
2150 }
2151 v
2152 }
2153 Thunk::ScanBackward {
2154 outer_init_off,
2155 outer_traj_off,
2156 outer_upstream_off,
2157 outer_xs_offs,
2158 ..
2159 } => {
2160 let mut v = vec![*outer_init_off, *outer_traj_off, *outer_upstream_off];
2161 for (off, _) in outer_xs_offs.iter() {
2162 v.push(*off);
2163 }
2164 v
2165 }
2166 Thunk::ScanBackwardXs {
2167 outer_init_off,
2168 outer_traj_off,
2169 outer_upstream_off,
2170 outer_xs_offs,
2171 ..
2172 } => {
2173 let mut v = vec![*outer_init_off, *outer_traj_off, *outer_upstream_off];
2174 for (off, _) in outer_xs_offs.iter() {
2175 v.push(*off);
2176 }
2177 v
2178 }
2179 Thunk::CustomFn { inputs, .. } => {
2180 inputs.iter().map(|(_, outer_off, _)| *outer_off).collect()
2181 }
2182 Thunk::ActivationInPlace { data, .. } => vec![*data],
2183 Thunk::LayerNorm { src, g, b, .. } | Thunk::GroupNorm { src, g, b, .. } => {
2184 vec![*src, *g, *b]
2185 }
2186 Thunk::BatchNormInference {
2187 src,
2188 g,
2189 b,
2190 mean,
2191 var,
2192 ..
2193 } => vec![*src, *g, *b, *mean, *var],
2194 Thunk::ResizeNearest2x { src, .. } => vec![*src],
2195 Thunk::AxialRope2d { src, .. } => vec![*src],
2196 Thunk::FusedResidualLN {
2197 x, res, bias, g, b, ..
2198 } => vec![*x, *res, *bias, *g, *b],
2199 Thunk::FusedResidualRmsNorm {
2200 x, res, bias, g, b, ..
2201 } => vec![*x, *res, *bias, *g, *b],
2202 Thunk::RmsNorm { src, g, b, .. } => vec![*src, *g, *b],
2203 Thunk::Softmax { data, .. } => vec![*data],
2204 Thunk::Cumsum { src, .. } => vec![*src],
2205 Thunk::Sample { logits, .. } => vec![*logits],
2206 Thunk::RngNormal { .. } | Thunk::RngUniform { .. } => vec![],
2207 Thunk::LoraMatMul { x, w, a, b, .. } => vec![*x, *w, *a, *b],
2208 Thunk::DequantMatMul {
2209 x, w_q, scale, zp, ..
2210 } => vec![*x, *w_q, *scale, *zp],
2211 Thunk::DequantMatMulGguf { x, w_q, .. } => vec![*x, *w_q],
2212 Thunk::DequantMatMulInt4 {
2213 x, w_q, scale, zp, ..
2214 } => vec![*x, *w_q, *scale, *zp],
2215 Thunk::DequantMatMulFp8 { x, w_q, scale, .. } => vec![*x, *w_q, *scale],
2216 Thunk::DequantMatMulNvfp4 {
2217 x,
2218 w_q,
2219 scale,
2220 global_scale,
2221 ..
2222 } => vec![*x, *w_q, *scale, *global_scale],
2223 Thunk::ScaledMatMul {
2224 lhs,
2225 rhs,
2226 lhs_scale,
2227 rhs_scale,
2228 bias,
2229 has_bias,
2230 ..
2231 } => {
2232 let mut v = vec![*lhs, *rhs, *lhs_scale, *rhs_scale];
2233 if *has_bias {
2234 v.push(*bias);
2235 }
2236 v
2237 }
2238 Thunk::ScaledQuantize { x, scale, .. } => vec![*x, *scale],
2239 Thunk::ScaledQuantScale { x, .. } => vec![*x],
2240 Thunk::ScaledDequantize { codes, scale, .. } => vec![*codes, *scale],
2241 Thunk::Conv2D1x1 { src, weight, .. } => vec![*src, *weight],
2242 Thunk::SelectiveScan {
2243 x, delta, a, b, c, ..
2244 } => vec![*x, *delta, *a, *b, *c],
2245 Thunk::GatedDeltaNet {
2246 q,
2247 k,
2248 v,
2249 g,
2250 beta,
2251 state,
2252 ..
2253 } => {
2254 let mut v = vec![*q, *k, *v, *g, *beta];
2255 if *state != 0 {
2256 v.push(*state);
2257 }
2258 v
2259 }
2260 Thunk::Attention { q, k, v, mask, .. } => vec![*q, *k, *v, *mask],
2261 Thunk::AttentionBackward {
2262 q, k, v, dy, mask, ..
2263 } => {
2264 let mut v = vec![*q, *k, *v, *dy];
2265 if *mask != 0 {
2266 v.push(*mask);
2267 }
2268 v
2269 }
2270 Thunk::Rope { src, cos, sin, .. } => vec![*src, *cos, *sin],
2271 Thunk::FusedAttnBlock {
2272 hidden,
2273 qkv_w,
2274 out_w,
2275 mask,
2276 qkv_b,
2277 out_b,
2278 cos,
2279 sin,
2280 ..
2281 } => vec![*hidden, *qkv_w, *out_w, *mask, *qkv_b, *out_b, *cos, *sin],
2282 Thunk::FusedSwiGLU { src, .. } => vec![*src],
2283 Thunk::Concat { inputs, .. } => inputs.iter().map(|(off, _, _)| *off).collect(),
2284 Thunk::ConcatF64 { inputs, .. } => inputs.iter().map(|(off, _, _)| *off).collect(),
2285 Thunk::Narrow { src, .. } => vec![*src],
2286 Thunk::Copy { src, .. } => vec![*src],
2287 Thunk::Gather { table, idx, .. } => vec![*table, *idx],
2288 _ => vec![],
2292 }
2293}
2294
2295#[allow(clippy::too_many_arguments)]
2309pub fn dequant_matmul_int8(
2310 x: &[f32], w_bytes: &[i8], scales: &[f32], zps: &[f32], out: &mut [f32], m: usize,
2316 k: usize,
2317 n: usize,
2318 block_size: usize,
2319 asym: bool,
2320) {
2321 let blocks_per_col = k.div_ceil(block_size);
2322 for i in 0..m {
2323 for j in 0..n {
2324 let mut acc = 0f32;
2325 for p in 0..k {
2326 let block = p / block_size;
2327 let s = scales[block * n + j];
2328 let z = if asym { zps[block * n + j] } else { 0.0 };
2329 let q = w_bytes[p * n + j] as f32;
2330 let dequantized = (q - z) * s;
2331 acc += x[i * k + p] * dequantized;
2332 }
2333 out[i * n + j] = acc;
2334 }
2335 }
2336 let _ = blocks_per_col;
2337}
2338
2339#[allow(clippy::too_many_arguments)]
2340fn dequant_matmul_int4(
2341 x: &[f32],
2342 w_bytes: &[u8],
2343 scales: &[f32],
2344 zps: &[f32],
2345 out: &mut [f32],
2346 m: usize,
2347 k: usize,
2348 n: usize,
2349 block_size: usize,
2350 asym: bool,
2351) {
2352 for i in 0..m {
2353 for j in 0..n {
2354 let mut acc = 0f32;
2355 for p in 0..k {
2356 let block = p / block_size;
2357 let s = scales[block * n + j];
2358 let z = if asym { zps[block * n + j] } else { 0.0 };
2359 let byte_idx = (p * n + j) / 2;
2360 let nibble = if (p * n + j) & 1 == 0 {
2361 w_bytes[byte_idx] & 0x0F
2362 } else {
2363 w_bytes[byte_idx] >> 4
2364 };
2365 let dequantized = (nibble as f32 - z) * s;
2366 acc += x[i * k + p] * dequantized;
2367 }
2368 out[i * n + j] = acc;
2369 }
2370 }
2371}
2372
2373fn fp8_e4m3_to_f32(b: u8) -> f32 {
2374 let sign = if b & 0x80 != 0 { -1.0 } else { 1.0 };
2375 let exp = (b >> 3) & 0x0F;
2376 let mant = b & 0x07;
2377 if exp == 0 {
2378 if mant == 0 {
2379 return 0.0;
2380 }
2381 return sign * (mant as f32) * 2f32.powi(-9);
2382 }
2383 if exp == 0x0F {
2384 return if mant == 0 {
2385 sign * f32::INFINITY
2386 } else {
2387 f32::NAN
2388 };
2389 }
2390 sign * (1.0 + mant as f32 / 8.0) * 2f32.powi(exp as i32 - 7)
2391}
2392
2393fn fp8_e5m2_to_f32(b: u8) -> f32 {
2394 let sign = if b & 0x80 != 0 { -1.0 } else { 1.0 };
2395 let exp = (b >> 2) & 0x1F;
2396 let mant = b & 0x03;
2397 if exp == 0 {
2398 if mant == 0 {
2399 return 0.0;
2400 }
2401 return sign * (mant as f32) * 2f32.powi(-16);
2402 }
2403 if exp == 0x1F {
2404 return if mant == 0 {
2405 sign * f32::INFINITY
2406 } else {
2407 f32::NAN
2408 };
2409 }
2410 sign * (1.0 + mant as f32 / 4.0) * 2f32.powi(exp as i32 - 15)
2411}
2412
2413#[allow(clippy::too_many_arguments)]
2414fn dequant_matmul_fp8(
2415 x: &[f32],
2416 w_bytes: &[u8],
2417 scales: &[f32],
2418 out: &mut [f32],
2419 m: usize,
2420 k: usize,
2421 n: usize,
2422 e5m2: bool,
2423) {
2424 let dequant = if e5m2 {
2425 fp8_e5m2_to_f32
2426 } else {
2427 fp8_e4m3_to_f32
2428 };
2429 for i in 0..m {
2430 for j in 0..n {
2431 let mut acc = 0f32;
2432 for p in 0..k {
2433 let w = dequant(w_bytes[p * n + j]);
2434 let s = scales.get(j).copied().unwrap_or(1.0);
2435 acc += x[i * k + p] * w * s;
2436 }
2437 out[i * n + j] = acc;
2438 }
2439 }
2440}
2441
2442#[allow(clippy::too_many_arguments)]
2443pub fn dequant_matmul_nvfp4(
2444 x: &[f32],
2445 w_bytes: &[u8],
2446 scale_bytes: &[u8],
2447 global_scale: f32,
2448 out: &mut [f32],
2449 m: usize,
2450 k: usize,
2451 n: usize,
2452) {
2453 use rlx_ir::{NVFP4_GROUP_SIZE, fp4_e2m1_to_f32, fp8_e4m3_scale_to_f32};
2454 let gs = NVFP4_GROUP_SIZE;
2455 for i in 0..m {
2456 for j in 0..n {
2457 let mut acc = 0f32;
2458 for p in 0..k {
2459 let byte_idx = (p * n + j) / 2;
2460 let nibble = if (p * n + j) & 1 == 0 {
2461 w_bytes[byte_idx] & 0x0F
2462 } else {
2463 w_bytes[byte_idx] >> 4
2464 };
2465 let block = p / gs;
2466 let scale = fp8_e4m3_scale_to_f32(scale_bytes[block * n + j]);
2467 let w = fp4_e2m1_to_f32(nibble) * scale * global_scale;
2468 acc += x[i * k + p] * w;
2469 }
2470 out[i * n + j] = acc;
2471 }
2472 }
2473}
2474
2475#[inline]
2485fn lowp_nblk(len: usize, layout: rlx_ir::ScaleLayout) -> usize {
2486 match layout {
2487 rlx_ir::ScaleLayout::PerTensor => 1,
2488 _ => len.div_ceil(layout.block() as usize),
2489 }
2490}
2491
2492#[inline]
2495fn lowp_snap_scale(layout: rlx_ir::ScaleLayout, s: f32) -> f32 {
2496 use rlx_ir::lowp_codec;
2497 match layout {
2498 rlx_ir::ScaleLayout::PerTensor => s,
2499 rlx_ir::ScaleLayout::BlockMxE8M0 { .. } => {
2500 lowp_codec::e8m0_to_f32(lowp_codec::f32_to_e8m0(s))
2501 }
2502 rlx_ir::ScaleLayout::Nvfp4 { .. } => lowp_codec::decode(
2503 rlx_ir::ScaledFormat::F8E4M3,
2504 lowp_codec::encode(rlx_ir::ScaledFormat::F8E4M3, s),
2505 ),
2506 }
2507}
2508
2509#[inline]
2511fn lowp_scale_at(
2512 layout: rlx_ir::ScaleLayout,
2513 scales: &[f32],
2514 free: usize,
2515 contract: usize,
2516 nblk: usize,
2517) -> f32 {
2518 match layout {
2519 rlx_ir::ScaleLayout::PerTensor => scales.first().copied().unwrap_or(1.0),
2520 _ => scales[free * nblk + contract / layout.block() as usize],
2521 }
2522}
2523
2524fn lowp_compute_scales(
2527 x: &[f32],
2528 fmt: rlx_ir::ScaledFormat,
2529 layout: rlx_ir::ScaleLayout,
2530 rows: usize,
2531 cols: usize,
2532) -> Vec<f32> {
2533 let maxf = fmt.max_finite();
2534 let to_scale = |amax: f32| if amax > 0.0 { amax / maxf } else { 1.0 };
2535 match layout {
2536 rlx_ir::ScaleLayout::PerTensor => {
2537 let amax = x.iter().fold(0.0f32, |a, &v| a.max(v.abs()));
2538 vec![to_scale(amax)]
2539 }
2540 _ => {
2541 let block = layout.block() as usize;
2542 let nblk = cols.div_ceil(block);
2543 let mut out = vec![1.0f32; rows * nblk];
2544 for r in 0..rows {
2545 for b in 0..nblk {
2546 let lo = b * block;
2547 let hi = (lo + block).min(cols);
2548 let mut amax = 0.0f32;
2549 for c in lo..hi {
2550 amax = amax.max(x[r * cols + c].abs());
2551 }
2552 out[r * nblk + b] = lowp_snap_scale(layout, to_scale(amax));
2553 }
2554 }
2555 out
2556 }
2557 }
2558}
2559
2560fn lowp_quantize(
2563 x: &[f32],
2564 scales: &[f32],
2565 fmt: rlx_ir::ScaledFormat,
2566 layout: rlx_ir::ScaleLayout,
2567 rows: usize,
2568 cols: usize,
2569 out: &mut [u8],
2570) {
2571 let nblk = lowp_nblk(cols, layout);
2572 for r in 0..rows {
2573 for c in 0..cols {
2574 let s = lowp_scale_at(layout, scales, r, c, nblk);
2575 let v = if s != 0.0 { x[r * cols + c] / s } else { 0.0 };
2576 out[r * cols + c] = rlx_ir::lowp_codec::encode(fmt, v);
2577 }
2578 }
2579}
2580
2581#[allow(clippy::too_many_arguments)]
2583fn lowp_scaled_matmul(
2584 lhs: &[u8],
2585 rhs: &[u8],
2586 lhs_scales: &[f32],
2587 rhs_scales: &[f32],
2588 bias: Option<&[f32]>,
2589 out: &mut [f32],
2590 m: usize,
2591 n: usize,
2592 k: usize,
2593 layout: rlx_ir::ScaleLayout,
2594 lhs_fmt: rlx_ir::ScaledFormat,
2595 rhs_fmt: rlx_ir::ScaledFormat,
2596) {
2597 use rlx_ir::lowp_codec::decode;
2598 let nblk = lowp_nblk(k, layout);
2599 for i in 0..m {
2600 for j in 0..n {
2601 let mut acc = 0f32;
2602 for p in 0..k {
2603 let a =
2604 decode(lhs_fmt, lhs[i * k + p]) * lowp_scale_at(layout, lhs_scales, i, p, nblk);
2605 let b =
2606 decode(rhs_fmt, rhs[j * k + p]) * lowp_scale_at(layout, rhs_scales, j, p, nblk);
2607 acc += a * b;
2608 }
2609 out[i * n + j] = acc + bias.map_or(0.0, |bb| bb[j]);
2610 }
2611 }
2612}
2613
2614fn lowp_dequantize(
2617 codes: &[u8],
2618 scales: &[f32],
2619 fmt: rlx_ir::ScaledFormat,
2620 layout: rlx_ir::ScaleLayout,
2621 rows: usize,
2622 cols: usize,
2623 out: &mut [f32],
2624) {
2625 use rlx_ir::lowp_codec::decode;
2626 let nblk = lowp_nblk(cols, layout);
2627 for r in 0..rows {
2628 for c in 0..cols {
2629 let s = lowp_scale_at(layout, scales, r, c, nblk);
2630 out[r * cols + c] = decode(fmt, codes[r * cols + c]) * s;
2631 }
2632 }
2633}
2634
2635unsafe fn lowp_read_scales(
2638 layout: rlx_ir::ScaleLayout,
2639 base: *mut u8,
2640 offset: usize,
2641 n: usize,
2642) -> Vec<f32> {
2643 use rlx_ir::lowp_codec;
2644 match layout {
2645 rlx_ir::ScaleLayout::PerTensor => {
2646 unsafe { std::slice::from_raw_parts(base.add(offset) as *const f32, n) }.to_vec()
2647 }
2648 rlx_ir::ScaleLayout::BlockMxE8M0 { .. } => {
2649 let bytes = unsafe { std::slice::from_raw_parts(base.add(offset), n) };
2650 bytes.iter().map(|&b| lowp_codec::e8m0_to_f32(b)).collect()
2651 }
2652 rlx_ir::ScaleLayout::Nvfp4 { .. } => {
2653 let bytes = unsafe { std::slice::from_raw_parts(base.add(offset), n) };
2654 bytes
2655 .iter()
2656 .map(|&b| lowp_codec::decode(rlx_ir::ScaledFormat::F8E4M3, b))
2657 .collect()
2658 }
2659 }
2660}
2661
2662fn sample_row(
2671 logits: &[f32],
2672 top_k: usize,
2673 top_p: f32,
2674 temperature: f32,
2675 rng: &mut rlx_ir::Philox4x32,
2676) -> usize {
2677 let v = logits.len();
2678 if v == 0 {
2679 return 0;
2680 }
2681 let temp = temperature.max(1e-6);
2682 let mut scaled: Vec<f32> = logits.iter().map(|&x| x / temp).collect();
2684
2685 if top_k > 0 && top_k < v {
2687 let mut indexed: Vec<(usize, f32)> = scaled.iter().copied().enumerate().collect();
2689 indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
2692 let cutoff = indexed[top_k - 1].1;
2693 for x in scaled.iter_mut() {
2694 if *x < cutoff {
2695 *x = f32::NEG_INFINITY;
2696 }
2697 }
2698 }
2699
2700 let mut max_l = f32::NEG_INFINITY;
2702 for &x in &scaled {
2703 if x > max_l {
2704 max_l = x;
2705 }
2706 }
2707 let mut sum = 0.0f32;
2708 for x in scaled.iter_mut() {
2709 *x = (*x - max_l).exp();
2710 sum += *x;
2711 }
2712 let inv = 1.0 / sum.max(f32::MIN_POSITIVE);
2713 for x in scaled.iter_mut() {
2714 *x *= inv;
2715 }
2716
2717 if top_p < 1.0 {
2720 let mut indexed: Vec<(usize, f32)> = scaled.iter().copied().enumerate().collect();
2721 indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
2722 let mut cum = 0.0f32;
2723 let mut keep = vec![false; v];
2724 for (idx, p) in indexed.iter() {
2725 keep[*idx] = true;
2726 cum += *p;
2727 if cum >= top_p {
2728 break;
2729 }
2730 }
2731 let mut new_sum = 0.0f32;
2732 for (i, x) in scaled.iter_mut().enumerate() {
2733 if !keep[i] {
2734 *x = 0.0;
2735 }
2736 new_sum += *x;
2737 }
2738 let inv = 1.0 / new_sum.max(f32::MIN_POSITIVE);
2739 for x in scaled.iter_mut() {
2740 *x *= inv;
2741 }
2742 }
2743
2744 let r = rng.next_f32();
2746 let mut acc = 0.0f32;
2747 for (i, &p) in scaled.iter().enumerate() {
2748 acc += p;
2749 if r <= acc {
2750 return i;
2751 }
2752 }
2753 v - 1 }
2755
2756#[inline]
2760fn apply_synthetic_mask(
2761 scores: &mut [f32],
2762 q_seq: usize,
2763 k_seq: usize,
2764 kind: rlx_ir::op::MaskKind,
2765) {
2766 let neg = crate::config::RuntimeConfig::global().attn_mask_neg_inf;
2767 let q_offset = k_seq.saturating_sub(q_seq);
2768 match kind {
2769 rlx_ir::op::MaskKind::None | rlx_ir::op::MaskKind::Custom | rlx_ir::op::MaskKind::Bias => {}
2770 rlx_ir::op::MaskKind::Causal => {
2771 for qi in 0..q_seq {
2772 let abs_q = q_offset + qi;
2773 for ki in (abs_q + 1)..k_seq {
2774 scores[qi * k_seq + ki] = neg;
2775 }
2776 }
2777 }
2778 rlx_ir::op::MaskKind::SlidingWindow(w) => {
2779 for qi in 0..q_seq {
2780 let abs_q = q_offset + qi;
2781 let lo = abs_q.saturating_sub(w);
2782 for ki in 0..k_seq {
2783 if ki < lo || ki > abs_q {
2784 scores[qi * k_seq + ki] = neg;
2785 }
2786 }
2787 }
2788 }
2789 }
2790}
2791
2792fn conv_nchw_dims(shape: &Shape) -> (u32, u32, u32, u32) {
2794 match shape.rank() {
2795 3 => (
2796 shape.dim(0).unwrap_static() as u32,
2797 shape.dim(1).unwrap_static() as u32,
2798 1,
2799 shape.dim(2).unwrap_static() as u32,
2800 ),
2801 4 => (
2802 shape.dim(0).unwrap_static() as u32,
2803 shape.dim(1).unwrap_static() as u32,
2804 shape.dim(2).unwrap_static() as u32,
2805 shape.dim(3).unwrap_static() as u32,
2806 ),
2807 r => panic!("conv_nchw_dims: expected rank 3 or 4, got {r}"),
2808 }
2809}
2810
2811pub fn compile_thunks(graph: &Graph, arena: &Arena) -> ThunkSchedule {
2813 compile_thunks_with_rng(graph, arena, rlx_ir::RngOptions::default())
2814}
2815
2816pub fn compile_thunks_with_rng(
2818 graph: &Graph,
2819 arena: &Arena,
2820 rng: rlx_ir::RngOptions,
2821) -> ThunkSchedule {
2822 let rng_shared = Arc::new(std::sync::RwLock::new(rng));
2823 let mut thunks = Vec::with_capacity(graph.len());
2824
2825 let mut use_counts: std::collections::HashMap<NodeId, usize> = std::collections::HashMap::new();
2831 for n in graph.nodes() {
2832 for &i in &n.inputs {
2833 *use_counts.entry(i).or_insert(0) += 1;
2834 }
2835 }
2836 let is_t2 = |g: &Graph, id: NodeId| -> bool {
2837 matches!(&g.node(id).op, Op::Transpose { perm } if perm.as_slice() == [1, 0])
2838 && g.node(id).shape.rank() == 2
2839 };
2840 let mut folded_transpose: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
2841 let mut matmul_fold: std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)> =
2842 std::collections::HashMap::new();
2843 for n in graph.nodes() {
2844 if !matches!(n.op, Op::MatMul) {
2845 continue;
2846 }
2847 let (a_id, b_id) = (n.inputs[0], n.inputs[1]);
2848 if graph.node(a_id).shape.rank() != 2
2849 || graph.node(b_id).shape.rank() != 2
2850 || n.shape.dtype() != rlx_ir::DType::F32
2851 {
2852 continue;
2853 }
2854 let fold_a = is_t2(graph, a_id) && use_counts.get(&a_id) == Some(&1);
2855 let fold_b = is_t2(graph, b_id) && use_counts.get(&b_id) == Some(&1);
2856 if !fold_a && !fold_b {
2857 continue;
2858 }
2859 let (asrc, ta) = if fold_a {
2860 (graph.node(a_id).inputs[0], true)
2861 } else {
2862 (a_id, false)
2863 };
2864 let (bsrc, tb) = if fold_b {
2865 (graph.node(b_id).inputs[0], true)
2866 } else {
2867 (b_id, false)
2868 };
2869 matmul_fold.insert(n.id, (asrc, ta, bsrc, tb));
2870 if fold_a {
2871 folded_transpose.insert(a_id);
2872 }
2873 if fold_b {
2874 folded_transpose.insert(b_id);
2875 }
2876 }
2877
2878 let out_set: std::collections::HashSet<NodeId> = graph.outputs.iter().copied().collect();
2889 let const_scalar = |g: &Graph, id: NodeId, n: usize| -> Option<f32> {
2890 if let Op::Constant { data } = &g.node(id).op {
2891 if data.len() == n * 4 {
2892 return Some(f32::from_le_bytes([data[0], data[1], data[2], data[3]]));
2893 }
2894 }
2895 None
2896 };
2897 let mut sgd_fold: std::collections::HashMap<
2899 NodeId,
2900 (NodeId, NodeId, NodeId, NodeId, f32, f32, usize),
2901 > = std::collections::HashMap::new();
2902 let mut sgd_elim: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
2903 for n in graph.nodes() {
2904 if !matches!(n.op, Op::Binary(BinaryOp::Sub)) || n.shape.dtype() != rlx_ir::DType::F32 {
2906 continue;
2907 }
2908 if !out_set.contains(&n.id) {
2909 continue;
2910 }
2911 let len = match n.shape.num_elements() {
2912 Some(l) => l,
2913 None => continue,
2914 };
2915 let (param, lr_v) = (n.inputs[0], n.inputs[1]);
2916 let lrv = graph.node(lr_v);
2918 if !matches!(lrv.op, Op::Binary(BinaryOp::Mul)) || use_counts.get(&lr_v) != Some(&1) {
2919 continue;
2920 }
2921 let (v_new, lr_c) = (lrv.inputs[0], lrv.inputs[1]);
2922 let lr = match const_scalar(graph, lr_c, len) {
2923 Some(v) => v,
2924 None => continue,
2925 };
2926 let vnew = graph.node(v_new);
2928 if !matches!(vnew.op, Op::Binary(BinaryOp::Add))
2929 || use_counts.get(&v_new) != Some(&1)
2930 || !out_set.contains(&v_new)
2931 {
2932 continue;
2933 }
2934 let (v_scaled, grad) = (vnew.inputs[0], vnew.inputs[1]);
2935 let vs = graph.node(v_scaled);
2937 if !matches!(vs.op, Op::Binary(BinaryOp::Mul)) || use_counts.get(&v_scaled) != Some(&1) {
2938 continue;
2939 }
2940 let (vel, mom_c) = (vs.inputs[0], vs.inputs[1]);
2941 let mom = match const_scalar(graph, mom_c, len) {
2942 Some(v) => v,
2943 None => continue,
2944 };
2945 sgd_fold.insert(n.id, (param, vel, grad, v_new, lr, mom, len));
2946 sgd_elim.insert(v_scaled);
2947 sgd_elim.insert(v_new);
2948 sgd_elim.insert(lr_v);
2949 }
2950
2951 for node in graph.nodes() {
2952 if rlx_opt::is_pure_view(graph, node) {
2956 thunks.push(Thunk::Nop);
2957 continue;
2958 }
2959 if folded_transpose.contains(&node.id) {
2961 thunks.push(Thunk::Nop);
2962 continue;
2963 }
2964 if sgd_elim.contains(&node.id) {
2966 thunks.push(Thunk::Nop);
2967 continue;
2968 }
2969 let t = match &node.op {
2970 Op::Input { .. } | Op::Param { .. } | Op::Constant { .. } => Thunk::Nop,
2971
2972 Op::FusedMatMulBiasAct { activation } => {
2973 let shape = &node.shape;
2974 let n = shape.dim(shape.rank() - 1).unwrap_static();
2975 let total = shape.num_elements().unwrap();
2976 let m = total / n;
2977 let a_len = get_len(graph, node.inputs[0]);
2978 let k = a_len / m;
2979 Thunk::FusedMmBiasAct {
2980 a: node_offset(arena, node.inputs[0]),
2981 w: node_offset(arena, node.inputs[1]),
2982 bias: node_offset(arena, node.inputs[2]),
2983 c: node_offset(arena, node.id),
2984 m: m as u32,
2985 k: k as u32,
2986 n: n as u32,
2987 act: *activation,
2988 }
2989 }
2990
2991 Op::FusedResidualLN { has_bias, eps } => {
2992 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
2993 let total = node.shape.num_elements().unwrap();
2994 let rows = total / h;
2995 let (g_idx, b_idx) = if *has_bias { (3, 4) } else { (2, 3) };
2996 Thunk::FusedResidualLN {
2997 x: node_offset(arena, node.inputs[0]),
2998 res: node_offset(arena, node.inputs[1]),
2999 bias: if *has_bias {
3000 node_offset(arena, node.inputs[2])
3001 } else {
3002 0
3003 },
3004 g: node_offset(arena, node.inputs[g_idx]),
3005 b: node_offset(arena, node.inputs[b_idx]),
3006 out: node_offset(arena, node.id),
3007 rows: rows as u32,
3008 h: h as u32,
3009 eps: *eps,
3010 has_bias: *has_bias,
3011 }
3012 }
3013
3014 Op::FusedResidualRmsNorm { has_bias, eps } => {
3015 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
3016 let total = node.shape.num_elements().unwrap();
3017 let rows = total / h;
3018 let (g_idx, b_idx) = if *has_bias { (3, 4) } else { (2, 3) };
3019 Thunk::FusedResidualRmsNorm {
3020 x: node_offset(arena, node.inputs[0]),
3021 res: node_offset(arena, node.inputs[1]),
3022 bias: if *has_bias {
3023 node_offset(arena, node.inputs[2])
3024 } else {
3025 0
3026 },
3027 g: node_offset(arena, node.inputs[g_idx]),
3028 b: node_offset(arena, node.inputs[b_idx]),
3029 out: node_offset(arena, node.id),
3030 rows: rows as u32,
3031 h: h as u32,
3032 eps: *eps,
3033 has_bias: *has_bias,
3034 }
3035 }
3036
3037 Op::MatMul => {
3038 let shape = &node.shape;
3039 let a_shape = &graph.node(node.inputs[0]).shape;
3040 let b_shape = &graph.node(node.inputs[1]).shape;
3041 let eff =
3044 rlx_ir::shape::matmul_shape(a_shape, b_shape).unwrap_or_else(|_| shape.clone());
3045 let rank = eff.rank().max(2);
3046 let n = eff.dim(rank - 1).unwrap_static();
3047 let k_dim = a_shape.dim(a_shape.rank().max(2) - 1).unwrap_static();
3048 if shape.dtype() == rlx_ir::DType::C64 {
3049 let both = a_shape.rank() >= 3 && b_shape.rank() >= 3;
3053 assert!(!both, "batched (both-operand) C64 matmul not yet supported");
3054 let m: usize = if a_shape.rank() >= 3 {
3055 (0..a_shape.rank() - 1)
3056 .map(|d| a_shape.dim(d).unwrap_static())
3057 .product()
3058 } else {
3059 a_shape.dim(a_shape.rank() - 2).unwrap_static()
3060 };
3061 Thunk::CgemmC64 {
3062 a: node_offset(arena, node.inputs[0]),
3063 b: node_offset(arena, node.inputs[1]),
3064 c: node_offset(arena, node.id),
3065 m: m as u32,
3066 k: k_dim as u32,
3067 n: n as u32,
3068 }
3069 } else {
3070 let both_batched = a_shape.rank() >= 3 && b_shape.rank() >= 3;
3073 let batched_3d =
3074 rank >= 3 && both_batched && a_shape.rank() + b_shape.rank() > 4;
3075 if batched_3d && shape.dtype() == rlx_ir::DType::F64 {
3076 let mut batch_prod = 1usize;
3077 for d in 0..rank - 2 {
3078 batch_prod *= eff.dim(d).unwrap_static();
3079 }
3080 let m_dim = eff.dim(rank - 2).unwrap_static();
3081 Thunk::BatchedDgemmF64 {
3082 a: node_offset(arena, node.inputs[0]),
3083 b: node_offset(arena, node.inputs[1]),
3084 c: node_offset(arena, node.id),
3085 batch: batch_prod as u32,
3086 m: m_dim as u32,
3087 k: k_dim as u32,
3088 n: n as u32,
3089 }
3090 } else if batched_3d && shape.dtype() == rlx_ir::DType::F32 {
3091 let mut batch_prod = 1usize;
3092 for d in 0..rank - 2 {
3093 batch_prod *= eff.dim(d).unwrap_static();
3094 }
3095 let m_dim = eff.dim(rank - 2).unwrap_static();
3096 Thunk::BatchedSgemm {
3097 a: node_offset(arena, node.inputs[0]),
3098 b: node_offset(arena, node.inputs[1]),
3099 c: node_offset(arena, node.id),
3100 batch: batch_prod as u32,
3101 m: m_dim as u32,
3102 k: k_dim as u32,
3103 n: n as u32,
3104 }
3105 } else {
3106 let m = if a_shape.rank() >= 3 && b_shape.rank() <= 2 {
3107 let mut m_prod = 1usize;
3108 for d in 0..a_shape.rank() - 1 {
3109 m_prod *= a_shape.dim(d).unwrap_static();
3110 }
3111 m_prod
3112 } else if a_shape.rank() >= 2 {
3113 a_shape.dim(a_shape.rank() - 2).unwrap_static()
3114 } else {
3115 eff.num_elements().unwrap_or(1) / n.max(1)
3116 };
3117 match shape.dtype() {
3118 rlx_ir::DType::F64 => Thunk::Dgemm {
3119 a: node_offset(arena, node.inputs[0]),
3120 b: node_offset(arena, node.inputs[1]),
3121 c: node_offset(arena, node.id),
3122 m: m as u32,
3123 k: k_dim as u32,
3124 n: n as u32,
3125 },
3126 _ => {
3127 if let Some(&(asrc, ta, bsrc, tb)) = matmul_fold.get(&node.id) {
3128 Thunk::SgemmT {
3131 a: node_offset(arena, asrc),
3132 b: node_offset(arena, bsrc),
3133 c: node_offset(arena, node.id),
3134 m: m as u32,
3135 k: k_dim as u32,
3136 n: n as u32,
3137 ta,
3138 tb,
3139 }
3140 } else {
3141 Thunk::Sgemm {
3142 a: node_offset(arena, node.inputs[0]),
3143 b: node_offset(arena, node.inputs[1]),
3144 c: node_offset(arena, node.id),
3145 m: m as u32,
3146 k: k_dim as u32,
3147 n: n as u32,
3148 }
3149 }
3150 }
3151 }
3152 }
3153 }
3154 }
3155
3156 Op::Binary(op) => {
3157 if let Some(&(param, vel, grad, v_new, lr, mom, len)) = sgd_fold.get(&node.id) {
3158 thunks.push(Thunk::SgdMomentum {
3161 param: node_offset(arena, param),
3162 vel: node_offset(arena, vel),
3163 grad: node_offset(arena, grad),
3164 p_out: node_offset(arena, node.id),
3165 v_out: node_offset(arena, v_new),
3166 lr,
3167 mom,
3168 len: len as u32,
3169 });
3170 continue;
3171 }
3172 let lhs_len = get_len(graph, node.inputs[0]);
3173 let rhs_len = get_len(graph, node.inputs[1]);
3174 let out_len = node.shape.num_elements().unwrap();
3175 if node.shape.dtype() == rlx_ir::DType::C64 {
3176 match op {
3180 BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div => {}
3181 BinaryOp::Max | BinaryOp::Min | BinaryOp::Pow => panic!(
3182 "Op::Binary({op:?}) on DType::C64: complex \
3183 max/min/pow have no single natural definition \
3184 — caller should drop to 2N-real-block (see \
3185 spike-ac) and pick a convention there"
3186 ),
3187 }
3188 }
3189 let (out_dims_bcast, bcast_lhs_strides, bcast_rhs_strides) =
3193 if lhs_len == out_len && rhs_len == out_len {
3194 (Vec::new(), Vec::new(), Vec::new())
3195 } else {
3196 let lhs_dims = get_static_dims(graph, node.inputs[0]);
3197 let rhs_dims = get_static_dims(graph, node.inputs[1]);
3198 let out_dims_v = get_static_dims(graph, node.id);
3199 if lhs_dims.is_empty() || rhs_dims.is_empty() || out_dims_v.is_empty() {
3200 (Vec::new(), Vec::new(), Vec::new())
3205 } else {
3206 let ls = broadcast_strides(&lhs_dims, &out_dims_v);
3207 let rs = broadcast_strides(&rhs_dims, &out_dims_v);
3208 let od: Vec<u32> = out_dims_v.iter().map(|x| *x as u32).collect();
3209 (od, ls, rs)
3210 }
3211 };
3212 if node.shape.dtype() == rlx_ir::DType::C64 {
3213 Thunk::BinaryFullC64 {
3214 lhs: node_offset(arena, node.inputs[0]),
3215 rhs: node_offset(arena, node.inputs[1]),
3216 dst: node_offset(arena, node.id),
3217 len: out_len as u32,
3218 lhs_len: lhs_len as u32,
3219 rhs_len: rhs_len as u32,
3220 op: *op,
3221 out_dims_bcast,
3222 bcast_lhs_strides,
3223 bcast_rhs_strides,
3224 }
3225 } else if node.shape.dtype() == rlx_ir::DType::F64 {
3226 Thunk::BinaryFullF64 {
3229 lhs: node_offset(arena, node.inputs[0]),
3230 rhs: node_offset(arena, node.inputs[1]),
3231 dst: node_offset(arena, node.id),
3232 len: out_len as u32,
3233 lhs_len: lhs_len as u32,
3234 rhs_len: rhs_len as u32,
3235 op: *op,
3236 out_dims_bcast,
3237 bcast_lhs_strides,
3238 bcast_rhs_strides,
3239 }
3240 } else if matches!(op, BinaryOp::Add)
3241 && rhs_len < out_len
3242 && out_len % rhs_len == 0
3243 && is_trailing_bias_broadcast(
3244 graph.node(node.inputs[1]).shape.dims(),
3245 graph.node(node.id).shape.dims(),
3246 )
3247 {
3248 Thunk::BiasAdd {
3258 src: node_offset(arena, node.inputs[0]),
3259 bias: node_offset(arena, node.inputs[1]),
3260 dst: node_offset(arena, node.id),
3261 m: (out_len / rhs_len) as u32,
3262 n: rhs_len as u32,
3263 }
3264 } else {
3265 let lhs_len = get_len(graph, node.inputs[0]);
3266 Thunk::BinaryFull {
3267 lhs: node_offset(arena, node.inputs[0]),
3268 rhs: node_offset(arena, node.inputs[1]),
3269 dst: node_offset(arena, node.id),
3270 len: out_len as u32,
3271 lhs_len: lhs_len as u32,
3272 rhs_len: rhs_len as u32,
3273 op: *op,
3274 out_dims_bcast,
3275 bcast_lhs_strides,
3276 bcast_rhs_strides,
3277 elem_bytes: node.shape.dtype().size_bytes() as u8,
3278 }
3279 }
3280 }
3281
3282 Op::Activation(act) => {
3283 let len = node.shape.num_elements().unwrap();
3284 let in_off = node_offset(arena, node.inputs[0]);
3285 let out_off = node_offset(arena, node.id);
3286 if node.shape.dtype() == rlx_ir::DType::C64 {
3287 match act {
3292 Activation::Neg | Activation::Exp | Activation::Log | Activation::Sqrt => {}
3293 other => panic!(
3294 "Op::Activation({other:?}) on DType::C64: no \
3295 natural complex extension — supported on C64: \
3296 Neg, Exp, Log, Sqrt"
3297 ),
3298 }
3299 Thunk::ActivationC64 {
3300 src: in_off,
3301 dst: out_off,
3302 len: len as u32,
3303 kind: *act,
3304 }
3305 } else if node.shape.dtype() == rlx_ir::DType::F64 {
3306 Thunk::ActivationF64 {
3307 src: in_off,
3308 dst: out_off,
3309 len: len as u32,
3310 kind: *act,
3311 }
3312 } else if in_off == out_off {
3313 Thunk::ActivationInPlace {
3317 data: out_off,
3318 len: len as u32,
3319 act: *act,
3320 }
3321 } else {
3322 thunks.push(Thunk::Copy {
3326 src: in_off,
3327 dst: out_off,
3328 len: len as u32,
3329 });
3330 Thunk::ActivationInPlace {
3331 data: out_off,
3332 len: len as u32,
3333 act: *act,
3334 }
3335 }
3336 }
3337
3338 Op::Gather { axis } if *axis == 0 => {
3339 let table_shape = &graph.node(node.inputs[0]).shape;
3340 let table_total = table_shape.num_elements().unwrap();
3341 let trailing: usize = (1..table_shape.rank())
3342 .map(|i| table_shape.dim(i).unwrap_static())
3343 .product();
3344 let idx_len = get_len(graph, node.inputs[1]);
3345 let idx_i64 =
3346 u8::from(graph.node(node.inputs[1]).shape.dtype() == rlx_ir::DType::I64);
3347 let table_bytes = graph.node(node.inputs[0]).shape.dtype().size_bytes() as u8;
3348 Thunk::Gather {
3349 table: node_offset(arena, node.inputs[0]),
3350 table_len: table_total as u32,
3351 idx: node_offset(arena, node.inputs[1]),
3352 dst: node_offset(arena, node.id),
3353 num_idx: idx_len as u32,
3354 trailing: trailing as u32,
3355 idx_i64,
3356 table_bytes,
3357 }
3358 }
3359
3360 Op::Gather { axis } => {
3361 let table_shape = &graph.node(node.inputs[0]).shape;
3363 let rank = table_shape.rank();
3364 let outer: usize = (0..*axis)
3365 .map(|i| table_shape.dim(i).unwrap_static())
3366 .product::<usize>()
3367 .max(1);
3368 let trailing: usize = (*axis + 1..rank)
3369 .map(|i| table_shape.dim(i).unwrap_static())
3370 .product::<usize>()
3371 .max(1);
3372 let axis_dim = table_shape.dim(*axis).unwrap_static();
3373 let idx_len = get_len(graph, node.inputs[1]);
3374 let idx_i64 =
3375 u8::from(graph.node(node.inputs[1]).shape.dtype() == rlx_ir::DType::I64);
3376 let table_bytes = graph.node(node.inputs[0]).shape.dtype().size_bytes() as u8;
3377 Thunk::GatherAxis {
3378 table: node_offset(arena, node.inputs[0]),
3379 idx: node_offset(arena, node.inputs[1]),
3380 dst: node_offset(arena, node.id),
3381 outer: outer as u32,
3382 axis_dim: axis_dim as u32,
3383 num_idx: idx_len as u32,
3384 trailing: trailing as u32,
3385 idx_i64,
3386 table_bytes,
3387 }
3388 }
3389
3390 Op::Narrow { axis, start, len } => {
3391 let in_shape = &graph.node(node.inputs[0]).shape;
3392 let elem_bytes = in_shape.dtype().size_bytes() as u8;
3393 let rank = in_shape.rank();
3394 let outer: usize = (0..*axis)
3395 .map(|i| in_shape.dim(i).unwrap_static())
3396 .product::<usize>()
3397 .max(1);
3398 let inner: usize = (*axis + 1..rank)
3399 .map(|i| in_shape.dim(i).unwrap_static())
3400 .product::<usize>()
3401 .max(1);
3402 let in_axis = in_shape.dim(*axis).unwrap_static();
3403 let src_byte_offset =
3404 node_offset(arena, node.inputs[0]) + start * inner * elem_bytes as usize;
3405 Thunk::Narrow {
3406 src: src_byte_offset,
3407 dst: node_offset(arena, node.id),
3408 outer: outer as u32,
3409 src_stride: (in_axis * inner) as u32, dst_stride: (*len * inner) as u32, inner: (*len * inner) as u32, elem_bytes,
3413 }
3414 }
3415
3416 Op::Reverse { axes } => {
3417 let in_shape = &graph.node(node.inputs[0]).shape;
3418 let rank = in_shape.rank();
3419 let dims: Vec<u32> = (0..rank)
3420 .map(|i| in_shape.dim(i).unwrap_static() as u32)
3421 .collect();
3422 let mut rev_mask = vec![false; rank];
3423 for &a in axes {
3424 if a < rank {
3425 rev_mask[a] = true;
3426 }
3427 }
3428 Thunk::Reverse {
3429 src: node_offset(arena, node.inputs[0]),
3430 dst: node_offset(arena, node.id),
3431 dims,
3432 rev_mask,
3433 elem_bytes: in_shape.dtype().size_bytes() as u8,
3434 }
3435 }
3436
3437 Op::Reshape { .. } | Op::StopGradient => {
3438 let len = node.shape.num_elements().unwrap();
3440 let src = node_offset(arena, node.inputs[0]);
3441 let dst = node_offset(arena, node.id);
3442 match node.shape.dtype() {
3443 rlx_ir::DType::F64 => Thunk::CopyF64 {
3444 src,
3445 dst,
3446 len: len as u32,
3447 },
3448 rlx_ir::DType::I64 => Thunk::CopyI64 {
3449 src,
3450 dst,
3451 len: len as u32,
3452 },
3453 _ => Thunk::Copy {
3454 src,
3455 dst,
3456 len: len as u32,
3457 },
3458 }
3459 }
3460
3461 Op::Cast { to } => {
3462 let in_node = graph.node(node.inputs[0]);
3463 let in_dtype = in_node.shape.dtype();
3464 let out_dtype = *to;
3465 let len = node.shape.num_elements().unwrap();
3466 let src = node_offset(arena, node.inputs[0]);
3467 let dst = node_offset(arena, node.id);
3468 if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::I64 {
3469 Thunk::CastF32ToI64 {
3470 src,
3471 dst,
3472 len: len as u32,
3473 }
3474 } else if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::F64 {
3475 Thunk::CastF32ToF64 {
3476 src,
3477 dst,
3478 len: len as u32,
3479 }
3480 } else if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::I32 {
3481 Thunk::CastF32ToI32 {
3482 src,
3483 dst,
3484 len: len as u32,
3485 }
3486 } else if in_dtype == rlx_ir::DType::I64 && out_dtype == rlx_ir::DType::F32 {
3487 Thunk::CastI64ToF32 {
3488 src,
3489 dst,
3490 len: len as u32,
3491 }
3492 } else if in_dtype == rlx_ir::DType::Bool && out_dtype == rlx_ir::DType::I32 {
3493 Thunk::CastBoolToI32 {
3494 src,
3495 dst,
3496 len: len as u32,
3497 }
3498 } else if in_dtype == rlx_ir::DType::Bool && out_dtype == rlx_ir::DType::F32 {
3499 Thunk::CastBoolToF32 {
3502 src,
3503 dst,
3504 len: len as u32,
3505 }
3506 } else if in_dtype == rlx_ir::DType::I32 && out_dtype == rlx_ir::DType::F32 {
3507 Thunk::CastI32ToF32 {
3508 src,
3509 dst,
3510 len: len as u32,
3511 }
3512 } else if in_dtype == out_dtype {
3513 match out_dtype {
3514 rlx_ir::DType::F64 => Thunk::CopyF64 {
3515 src,
3516 dst,
3517 len: len as u32,
3518 },
3519 rlx_ir::DType::I64 => Thunk::CopyI64 {
3520 src,
3521 dst,
3522 len: len as u32,
3523 },
3524 _ => Thunk::Copy {
3525 src,
3526 dst,
3527 len: len as u32,
3528 },
3529 }
3530 } else {
3531 Thunk::Copy {
3532 src,
3533 dst,
3534 len: len as u32,
3535 }
3536 }
3537 }
3538
3539 Op::Quantize {
3540 axis,
3541 scales,
3542 zero_points,
3543 } => {
3544 let (chan_axis, chan_dim, inner) = quant_layout(&node.shape, *axis);
3545 Thunk::Quantize {
3546 x: node_offset(arena, node.inputs[0]),
3547 q: node_offset(arena, node.id),
3548 len: node.shape.num_elements().unwrap() as u32,
3549 chan_axis: chan_axis as u32,
3550 chan_dim: chan_dim as u32,
3551 inner: inner as u32,
3552 scales: scales.clone(),
3553 zero_points: zero_points.clone(),
3554 }
3555 }
3556
3557 Op::FakeQuantize {
3558 bits,
3559 axis,
3560 ste,
3561 scale_mode,
3562 } => {
3563 let (chan_axis, chan_dim, inner) = quant_layout(&node.shape, *axis);
3564 let state_off = match scale_mode {
3565 rlx_ir::op::ScaleMode::PerBatch => None,
3566 rlx_ir::op::ScaleMode::EMA { .. } | rlx_ir::op::ScaleMode::Fixed => {
3567 debug_assert_eq!(
3569 node.inputs.len(),
3570 2,
3571 "EMA/Fixed FakeQuantize needs a state input"
3572 );
3573 Some(node_offset(arena, node.inputs[1]))
3574 }
3575 };
3576 Thunk::FakeQuantize {
3577 x: node_offset(arena, node.inputs[0]),
3578 out: node_offset(arena, node.id),
3579 len: node.shape.num_elements().unwrap() as u32,
3580 chan_axis: chan_axis as u32,
3581 chan_dim: chan_dim as u32,
3582 inner: inner as u32,
3583 bits: *bits,
3584 ste: *ste,
3585 scale_mode: *scale_mode,
3586 state_off,
3587 }
3588 }
3589
3590 Op::FakeQuantizeLSQ { bits, axis } => {
3591 let (chan_axis, chan_dim, inner) = quant_layout(&node.shape, *axis);
3592 Thunk::FakeQuantizeLSQ {
3593 x: node_offset(arena, node.inputs[0]),
3594 scale_off: node_offset(arena, node.inputs[1]),
3595 out: node_offset(arena, node.id),
3596 len: node.shape.num_elements().unwrap() as u32,
3597 chan_axis: chan_axis as u32,
3598 chan_dim: chan_dim as u32,
3599 inner: inner as u32,
3600 bits: *bits,
3601 }
3602 }
3603
3604 Op::FakeQuantizeLSQBackwardX { bits, axis } => {
3605 let (chan_axis, chan_dim, inner) = quant_layout(&node.shape, *axis);
3606 Thunk::FakeQuantizeLSQBackwardX {
3607 x: node_offset(arena, node.inputs[0]),
3608 scale_off: node_offset(arena, node.inputs[1]),
3609 dy: node_offset(arena, node.inputs[2]),
3610 dx: node_offset(arena, node.id),
3611 len: node.shape.num_elements().unwrap() as u32,
3612 chan_axis: chan_axis as u32,
3613 chan_dim: chan_dim as u32,
3614 inner: inner as u32,
3615 bits: *bits,
3616 }
3617 }
3618
3619 Op::FakeQuantizeLSQBackwardScale { bits, axis } => {
3620 let in_shape = &graph.node(node.inputs[0]).shape;
3623 let (chan_axis, chan_dim, inner) = quant_layout(in_shape, *axis);
3624 Thunk::FakeQuantizeLSQBackwardScale {
3625 x: node_offset(arena, node.inputs[0]),
3626 scale_off: node_offset(arena, node.inputs[1]),
3627 dy: node_offset(arena, node.inputs[2]),
3628 dscale: node_offset(arena, node.id),
3629 len: in_shape.num_elements().unwrap() as u32,
3630 chan_axis: chan_axis as u32,
3631 chan_dim: chan_dim as u32,
3632 inner: inner as u32,
3633 bits: *bits,
3634 }
3635 }
3636
3637 Op::FakeQuantizeBackward { bits, axis, ste } => {
3638 let (chan_axis, chan_dim, inner) = quant_layout(&node.shape, *axis);
3639 Thunk::FakeQuantizeBackward {
3640 x: node_offset(arena, node.inputs[0]),
3641 dy: node_offset(arena, node.inputs[1]),
3642 dx: node_offset(arena, node.id),
3643 len: node.shape.num_elements().unwrap() as u32,
3644 chan_axis: chan_axis as u32,
3645 chan_dim: chan_dim as u32,
3646 inner: inner as u32,
3647 bits: *bits,
3648 ste: *ste,
3649 }
3650 }
3651
3652 Op::Dequantize {
3653 axis,
3654 scales,
3655 zero_points,
3656 } => {
3657 let (chan_axis, chan_dim, inner) = quant_layout(&node.shape, *axis);
3658 Thunk::Dequantize {
3659 q: node_offset(arena, node.inputs[0]),
3660 x: node_offset(arena, node.id),
3661 len: node.shape.num_elements().unwrap() as u32,
3662 chan_axis: chan_axis as u32,
3663 chan_dim: chan_dim as u32,
3664 inner: inner as u32,
3665 scales: scales.clone(),
3666 zero_points: zero_points.clone(),
3667 }
3668 }
3669
3670 Op::Expand { .. } => {
3671 let in_shape = &graph.node(node.inputs[0]).shape;
3676 let out_shape = &node.shape;
3677 let in_rank = in_shape.rank();
3678 let out_rank = out_shape.rank();
3679 let pad = out_rank.saturating_sub(in_rank);
3681 let in_dims: Vec<usize> = (0..out_rank)
3682 .map(|i| {
3683 if i < pad {
3684 1
3685 } else {
3686 in_shape.dim(i - pad).unwrap_static()
3687 }
3688 })
3689 .collect();
3690 let mut in_strides_full = vec![1usize; out_rank];
3692 for d in (0..out_rank.saturating_sub(1)).rev() {
3693 in_strides_full[d] = in_strides_full[d + 1] * in_dims[d + 1];
3694 }
3695 let out_dims: Vec<u32> = (0..out_rank)
3696 .map(|i| out_shape.dim(i).unwrap_static() as u32)
3697 .collect();
3698 let in_strides: Vec<u32> = (0..out_rank)
3700 .map(|i| {
3701 if in_dims[i] == 1 && (out_dims[i] as usize) > 1 {
3702 0
3703 } else {
3704 in_strides_full[i] as u32
3705 }
3706 })
3707 .collect();
3708 let in_total = in_dims.iter().product::<usize>() as u32;
3709 let src = node_offset(arena, node.inputs[0]);
3710 let dst = node_offset(arena, node.id);
3711 let elem_bytes = node.shape.dtype().size_bytes() as u8;
3712 match node.shape.dtype() {
3713 rlx_ir::DType::F64 => Thunk::TransposeF64 {
3714 src,
3715 dst,
3716 in_total,
3717 out_dims,
3718 in_strides,
3719 },
3720 _ => Thunk::Transpose {
3721 src,
3722 dst,
3723 in_total,
3724 out_dims,
3725 in_strides,
3726 elem_bytes,
3727 },
3728 }
3729 }
3730
3731 Op::RmsNorm { eps, .. } => {
3732 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
3733 let total = node.shape.num_elements().unwrap();
3734 Thunk::RmsNorm {
3735 src: node_offset(arena, node.inputs[0]),
3736 g: node_offset(arena, node.inputs[1]),
3737 b: node_offset(arena, node.inputs[2]),
3738 dst: node_offset(arena, node.id),
3739 rows: (total / h) as u32,
3740 h: h as u32,
3741 eps: *eps,
3742 }
3743 }
3744
3745 Op::LayerNorm { eps, .. } => {
3746 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
3747 let total = node.shape.num_elements().unwrap();
3748 Thunk::LayerNorm {
3749 src: node_offset(arena, node.inputs[0]),
3750 g: node_offset(arena, node.inputs[1]),
3751 b: node_offset(arena, node.inputs[2]),
3752 dst: node_offset(arena, node.id),
3753 rows: (total / h) as u32,
3754 h: h as u32,
3755 eps: *eps,
3756 }
3757 }
3758
3759 Op::GroupNorm { num_groups, eps } => {
3760 let in_shape = &graph.node(node.inputs[0]).shape;
3761 let (n, c, h, w) = conv_nchw_dims(in_shape);
3762 Thunk::GroupNorm {
3763 src: node_offset(arena, node.inputs[0]),
3764 g: node_offset(arena, node.inputs[1]),
3765 b: node_offset(arena, node.inputs[2]),
3766 dst: node_offset(arena, node.id),
3767 n,
3768 c,
3769 h,
3770 w,
3771 num_groups: *num_groups as u32,
3772 eps: *eps,
3773 }
3774 }
3775
3776 Op::BatchNormInference { eps } => {
3777 let in_shape = &graph.node(node.inputs[0]).shape;
3778 let rank = in_shape.rank();
3779 let channels = in_shape.dim(rank - 1).unwrap_static();
3780 let total = in_shape.num_elements().unwrap_or(0);
3781 let count = (total / channels.max(1)) as u32;
3782 Thunk::BatchNormInference {
3783 src: node_offset(arena, node.inputs[0]),
3784 g: node_offset(arena, node.inputs[1]),
3785 b: node_offset(arena, node.inputs[2]),
3786 mean: node_offset(arena, node.inputs[3]),
3787 var: node_offset(arena, node.inputs[4]),
3788 dst: node_offset(arena, node.id),
3789 count,
3790 channels: channels as u32,
3791 eps: *eps,
3792 }
3793 }
3794
3795 Op::BatchNormInferenceBackwardInput { eps } => {
3796 let x_shape = &graph.node(node.inputs[0]).shape;
3797 let rank = x_shape.rank();
3798 let channels = x_shape.dim(rank - 1).unwrap_static();
3799 let total = x_shape.num_elements().unwrap_or(0);
3800 Thunk::BatchNormInferenceBackwardInput {
3801 x: node_offset(arena, node.inputs[0]),
3802 gamma: node_offset(arena, node.inputs[1]),
3803 mean: node_offset(arena, node.inputs[2]),
3804 var: node_offset(arena, node.inputs[3]),
3805 dy: node_offset(arena, node.inputs[4]),
3806 dx: node_offset(arena, node.id),
3807 count: (total / channels.max(1)) as u32,
3808 channels: channels as u32,
3809 eps: *eps,
3810 }
3811 }
3812
3813 Op::BatchNormInferenceBackwardGamma { eps } => {
3814 let x_shape = &graph.node(node.inputs[0]).shape;
3815 let rank = x_shape.rank();
3816 let channels = x_shape.dim(rank - 1).unwrap_static();
3817 let total = x_shape.num_elements().unwrap_or(0);
3818 let _gamma_shape = &graph.node(node.id).shape;
3819 Thunk::BatchNormInferenceBackwardGamma {
3820 x: node_offset(arena, node.inputs[0]),
3821 mean: node_offset(arena, node.inputs[1]),
3822 var: node_offset(arena, node.inputs[2]),
3823 dy: node_offset(arena, node.inputs[3]),
3824 dgamma: node_offset(arena, node.id),
3825 count: (total / channels.max(1)) as u32,
3826 channels: channels as u32,
3827 eps: *eps,
3828 }
3829 }
3830
3831 Op::BatchNormInferenceBackwardBeta => {
3832 let dy_shape = &graph.node(node.inputs[0]).shape;
3833 let rank = dy_shape.rank();
3834 let channels = dy_shape.dim(rank - 1).unwrap_static();
3835 let total = dy_shape.num_elements().unwrap_or(0);
3836 Thunk::BatchNormInferenceBackwardBeta {
3837 dy: node_offset(arena, node.inputs[0]),
3838 dbeta: node_offset(arena, node.id),
3839 count: (total / channels.max(1)) as u32,
3840 channels: channels as u32,
3841 }
3842 }
3843
3844 Op::LayerNorm2d { eps } => {
3845 let in_shape = &graph.node(node.inputs[0]).shape;
3846 let (n, c, h, w) = conv_nchw_dims(in_shape);
3847 Thunk::LayerNorm2d {
3848 src: node_offset(arena, node.inputs[0]),
3849 g: node_offset(arena, node.inputs[1]),
3850 b: node_offset(arena, node.inputs[2]),
3851 dst: node_offset(arena, node.id),
3852 n,
3853 c,
3854 h,
3855 w,
3856 eps: *eps,
3857 }
3858 }
3859
3860 Op::ConvTranspose2d {
3861 kernel_size,
3862 stride,
3863 padding,
3864 dilation,
3865 output_padding: _,
3866 groups,
3867 } => {
3868 let in_shape = &graph.node(node.inputs[0]).shape;
3869 let out_shape = &node.shape;
3870 let (n, c_in, h, w_in) = conv_nchw_dims(in_shape);
3871 let (_, c_out, h_out, w_out) = conv_nchw_dims(out_shape);
3872 Thunk::ConvTranspose2d {
3873 src: node_offset(arena, node.inputs[0]),
3874 weight: node_offset(arena, node.inputs[1]),
3875 dst: node_offset(arena, node.id),
3876 n,
3877 c_in,
3878 h,
3879 w_in,
3880 c_out,
3881 h_out,
3882 w_out,
3883 kh: kernel_size[0] as u32,
3884 kw: kernel_size[1] as u32,
3885 sh: stride.first().copied().unwrap_or(1) as u32,
3886 sw: stride.get(1).copied().unwrap_or(1) as u32,
3887 ph: padding.first().copied().unwrap_or(0) as u32,
3888 pw: padding.get(1).copied().unwrap_or(0) as u32,
3889 dh: dilation.first().copied().unwrap_or(1) as u32,
3890 dw: dilation.get(1).copied().unwrap_or(1) as u32,
3891 groups: *groups as u32,
3892 }
3893 }
3894
3895 Op::ResizeNearest2x => {
3896 let in_shape = &graph.node(node.inputs[0]).shape;
3897 let (n, c, h, w) = conv_nchw_dims(in_shape);
3898 Thunk::ResizeNearest2x {
3899 src: node_offset(arena, node.inputs[0]),
3900 dst: node_offset(arena, node.id),
3901 n,
3902 c,
3903 h,
3904 w,
3905 }
3906 }
3907
3908 Op::AxialRope2d {
3909 end_x,
3910 end_y,
3911 head_dim,
3912 num_heads,
3913 theta,
3914 repeat_factor,
3915 } => {
3916 let in_shape = &graph.node(node.inputs[0]).shape;
3917 let batch = in_shape.dim(0).unwrap_static() as u32;
3918 let seq = in_shape.dim(1).unwrap_static() as u32;
3919 let hidden = in_shape.dim(2).unwrap_static() as u32;
3920 Thunk::AxialRope2d {
3921 src: node_offset(arena, node.inputs[0]),
3922 dst: node_offset(arena, node.id),
3923 batch,
3924 seq,
3925 hidden,
3926 end_x: *end_x as u32,
3927 end_y: *end_y as u32,
3928 head_dim: *head_dim as u32,
3929 num_heads: *num_heads as u32,
3930 theta: *theta,
3931 repeat_factor: *repeat_factor as u32,
3932 }
3933 }
3934
3935 Op::Softmax { axis } => {
3936 let rank = node.shape.rank();
3937 let ax = if *axis < 0 {
3938 (rank as i32 + axis) as usize
3939 } else {
3940 *axis as usize
3941 };
3942 let cols = node.shape.dim(ax).unwrap_static();
3943 let total = node.shape.num_elements().unwrap();
3944 let in_off = node_offset(arena, node.inputs[0]);
3945 let out_off = node_offset(arena, node.id);
3946 if in_off != out_off {
3952 thunks.push(Thunk::Copy {
3953 src: in_off,
3954 dst: out_off,
3955 len: total as u32,
3956 });
3957 }
3958 Thunk::Softmax {
3959 data: out_off,
3960 rows: (total / cols) as u32,
3961 cols: cols as u32,
3962 }
3963 }
3964
3965 Op::SelectiveScan { state_size } => {
3966 let in_shape = &graph.node(node.inputs[0]).shape;
3967 let (batch, seq, hidden) = (
3968 in_shape.dim(0).unwrap_static(),
3969 in_shape.dim(1).unwrap_static(),
3970 in_shape.dim(2).unwrap_static(),
3971 );
3972 Thunk::SelectiveScan {
3973 x: node_offset(arena, node.inputs[0]),
3974 delta: node_offset(arena, node.inputs[1]),
3975 a: node_offset(arena, node.inputs[2]),
3976 b: node_offset(arena, node.inputs[3]),
3977 c: node_offset(arena, node.inputs[4]),
3978 dst: node_offset(arena, node.id),
3979 batch: batch as u32,
3980 seq: seq as u32,
3981 hidden: hidden as u32,
3982 state_size: *state_size as u32,
3983 }
3984 }
3985
3986 Op::GatedDeltaNet {
3987 state_size,
3988 carry_state,
3989 } => {
3990 let q_shape = &graph.node(node.inputs[0]).shape;
3991 let (batch, seq, heads) = (
3992 q_shape.dim(0).unwrap_static(),
3993 q_shape.dim(1).unwrap_static(),
3994 q_shape.dim(2).unwrap_static(),
3995 );
3996 let state_off = if *carry_state {
3997 node_offset(arena, node.inputs[5])
3998 } else {
3999 0
4000 };
4001 Thunk::GatedDeltaNet {
4002 q: node_offset(arena, node.inputs[0]),
4003 k: node_offset(arena, node.inputs[1]),
4004 v: node_offset(arena, node.inputs[2]),
4005 g: node_offset(arena, node.inputs[3]),
4006 beta: node_offset(arena, node.inputs[4]),
4007 state: state_off,
4008 dst: node_offset(arena, node.id),
4009 batch: batch as u32,
4010 seq: seq as u32,
4011 heads: heads as u32,
4012 state_size: *state_size as u32,
4013 }
4014 }
4015
4016 Op::Lstm {
4017 hidden_size,
4018 num_layers,
4019 bidirectional,
4020 carry,
4021 } => {
4022 let x_shape = &graph.node(node.inputs[0]).shape;
4023 let (batch, seq, input_size) = (
4024 x_shape.dim(0).unwrap_static(),
4025 x_shape.dim(1).unwrap_static(),
4026 x_shape.dim(2).unwrap_static(),
4027 );
4028 let (h0, c0) = if *carry {
4029 (
4030 node_offset(arena, node.inputs[4]),
4031 node_offset(arena, node.inputs[5]),
4032 )
4033 } else {
4034 (0, 0)
4035 };
4036 Thunk::Lstm {
4037 x: node_offset(arena, node.inputs[0]),
4038 w_ih: node_offset(arena, node.inputs[1]),
4039 w_hh: node_offset(arena, node.inputs[2]),
4040 bias: node_offset(arena, node.inputs[3]),
4041 h0,
4042 c0,
4043 dst: node_offset(arena, node.id),
4044 batch: batch as u32,
4045 seq: seq as u32,
4046 input_size: input_size as u32,
4047 hidden: *hidden_size as u32,
4048 num_layers: *num_layers as u32,
4049 bidirectional: *bidirectional,
4050 carry: *carry,
4051 }
4052 }
4053
4054 Op::Gru {
4055 hidden_size,
4056 num_layers,
4057 bidirectional,
4058 carry,
4059 } => {
4060 let x_shape = &graph.node(node.inputs[0]).shape;
4061 let (batch, seq, input_size) = (
4062 x_shape.dim(0).unwrap_static(),
4063 x_shape.dim(1).unwrap_static(),
4064 x_shape.dim(2).unwrap_static(),
4065 );
4066 let h0 = if *carry {
4068 node_offset(arena, node.inputs[5])
4069 } else {
4070 0
4071 };
4072 Thunk::Gru {
4073 x: node_offset(arena, node.inputs[0]),
4074 w_ih: node_offset(arena, node.inputs[1]),
4075 w_hh: node_offset(arena, node.inputs[2]),
4076 b_ih: node_offset(arena, node.inputs[3]),
4077 b_hh: node_offset(arena, node.inputs[4]),
4078 h0,
4079 dst: node_offset(arena, node.id),
4080 batch: batch as u32,
4081 seq: seq as u32,
4082 input_size: input_size as u32,
4083 hidden: *hidden_size as u32,
4084 num_layers: *num_layers as u32,
4085 bidirectional: *bidirectional,
4086 carry: *carry,
4087 }
4088 }
4089
4090 Op::Rnn {
4091 hidden_size,
4092 num_layers,
4093 bidirectional,
4094 carry,
4095 relu,
4096 } => {
4097 let x_shape = &graph.node(node.inputs[0]).shape;
4098 let (batch, seq, input_size) = (
4099 x_shape.dim(0).unwrap_static(),
4100 x_shape.dim(1).unwrap_static(),
4101 x_shape.dim(2).unwrap_static(),
4102 );
4103 let h0 = if *carry {
4105 node_offset(arena, node.inputs[4])
4106 } else {
4107 0
4108 };
4109 Thunk::Rnn {
4110 x: node_offset(arena, node.inputs[0]),
4111 w_ih: node_offset(arena, node.inputs[1]),
4112 w_hh: node_offset(arena, node.inputs[2]),
4113 bias: node_offset(arena, node.inputs[3]),
4114 h0,
4115 dst: node_offset(arena, node.id),
4116 batch: batch as u32,
4117 seq: seq as u32,
4118 input_size: input_size as u32,
4119 hidden: *hidden_size as u32,
4120 num_layers: *num_layers as u32,
4121 bidirectional: *bidirectional,
4122 carry: *carry,
4123 relu: *relu,
4124 }
4125 }
4126
4127 Op::Mamba2 {
4128 head_dim,
4129 state_size,
4130 } => {
4131 let x_shape = &graph.node(node.inputs[0]).shape;
4133 Thunk::Mamba2 {
4134 x: node_offset(arena, node.inputs[0]),
4135 dt: node_offset(arena, node.inputs[1]),
4136 a: node_offset(arena, node.inputs[2]),
4137 b: node_offset(arena, node.inputs[3]),
4138 c: node_offset(arena, node.inputs[4]),
4139 dst: node_offset(arena, node.id),
4140 batch: x_shape.dim(0).unwrap_static() as u32,
4141 seq: x_shape.dim(1).unwrap_static() as u32,
4142 heads: x_shape.dim(2).unwrap_static() as u32,
4143 head_dim: *head_dim as u32,
4144 state_size: *state_size as u32,
4145 }
4146 }
4147
4148 Op::QMatMul {
4149 x_zp,
4150 w_zp,
4151 out_zp,
4152 mult,
4153 } => {
4154 let x_shape = &graph.node(node.inputs[0]).shape;
4155 let w_shape = &graph.node(node.inputs[1]).shape;
4156 let m = x_shape.dim(0).unwrap_static();
4157 let k = x_shape.dim(1).unwrap_static();
4158 let n = w_shape.dim(1).unwrap_static();
4159 Thunk::QMatMul {
4160 x: node_offset(arena, node.inputs[0]),
4161 w: node_offset(arena, node.inputs[1]),
4162 bias: node_offset(arena, node.inputs[2]),
4163 out: node_offset(arena, node.id),
4164 m: m as u32,
4165 k: k as u32,
4166 n: n as u32,
4167 x_zp: *x_zp,
4168 w_zp: *w_zp,
4169 out_zp: *out_zp,
4170 mult: *mult,
4171 }
4172 }
4173
4174 Op::QConv2d {
4175 kernel_size,
4176 stride,
4177 padding,
4178 dilation,
4179 groups,
4180 x_zp,
4181 w_zp,
4182 out_zp,
4183 mult,
4184 } => {
4185 let in_shape = &graph.node(node.inputs[0]).shape;
4186 let w_shape = &graph.node(node.inputs[1]).shape;
4187 let out_shape = &node.shape;
4188 if kernel_size.len() == 2
4189 && in_shape.rank() == 4
4190 && w_shape.rank() == 4
4191 && out_shape.rank() == 4
4192 {
4193 Thunk::QConv2d {
4194 x: node_offset(arena, node.inputs[0]),
4195 w: node_offset(arena, node.inputs[1]),
4196 bias: node_offset(arena, node.inputs[2]),
4197 out: node_offset(arena, node.id),
4198 n: in_shape.dim(0).unwrap_static() as u32,
4199 c_in: in_shape.dim(1).unwrap_static() as u32,
4200 h: in_shape.dim(2).unwrap_static() as u32,
4201 w_in: in_shape.dim(3).unwrap_static() as u32,
4202 c_out: out_shape.dim(1).unwrap_static() as u32,
4203 h_out: out_shape.dim(2).unwrap_static() as u32,
4204 w_out: out_shape.dim(3).unwrap_static() as u32,
4205 kh: kernel_size[0] as u32,
4206 kw: kernel_size[1] as u32,
4207 sh: stride.first().copied().unwrap_or(1) as u32,
4208 sw: stride.get(1).copied().unwrap_or(1) as u32,
4209 ph: padding.first().copied().unwrap_or(0) as u32,
4210 pw: padding.get(1).copied().unwrap_or(0) as u32,
4211 dh: dilation.first().copied().unwrap_or(1) as u32,
4212 dw: dilation.get(1).copied().unwrap_or(1) as u32,
4213 groups: *groups as u32,
4214 x_zp: *x_zp,
4215 w_zp: *w_zp,
4216 out_zp: *out_zp,
4217 mult: *mult,
4218 }
4219 } else {
4220 Thunk::Nop
4221 }
4222 }
4223
4224 Op::DequantMatMul { scheme } => {
4225 use rlx_ir::quant::QuantScheme;
4226 let n = node.shape.dim(node.shape.rank() - 1).unwrap_static();
4227 let total = node.shape.num_elements().unwrap();
4228 let m = total / n.max(1);
4229 let x_total = graph.node(node.inputs[0]).shape.num_elements().unwrap();
4230 let k = x_total / m.max(1);
4231 if scheme.is_gguf() {
4232 Thunk::DequantMatMulGguf {
4233 x: node_offset(arena, node.inputs[0]),
4234 w_q: node_offset(arena, node.inputs[1]),
4235 dst: node_offset(arena, node.id),
4236 m: m as u32,
4237 k: k as u32,
4238 n: n as u32,
4239 scheme: *scheme,
4240 }
4241 } else {
4242 match scheme {
4243 QuantScheme::Nvfp4Block => Thunk::DequantMatMulNvfp4 {
4244 x: node_offset(arena, node.inputs[0]),
4245 w_q: node_offset(arena, node.inputs[1]),
4246 scale: node_offset(arena, node.inputs[2]),
4247 global_scale: node_offset(arena, node.inputs[3]),
4248 dst: node_offset(arena, node.id),
4249 m: m as u32,
4250 k: k as u32,
4251 n: n as u32,
4252 },
4253 QuantScheme::Int4Block { block_size } => Thunk::DequantMatMulInt4 {
4254 x: node_offset(arena, node.inputs[0]),
4255 w_q: node_offset(arena, node.inputs[1]),
4256 scale: node_offset(arena, node.inputs[2]),
4257 zp: node_offset(arena, node.inputs[3]),
4258 dst: node_offset(arena, node.id),
4259 m: m as u32,
4260 k: k as u32,
4261 n: n as u32,
4262 block_size: *block_size,
4263 is_asymmetric: false,
4264 },
4265 QuantScheme::Fp8E4m3 => Thunk::DequantMatMulFp8 {
4266 x: node_offset(arena, node.inputs[0]),
4267 w_q: node_offset(arena, node.inputs[1]),
4268 scale: node_offset(arena, node.inputs[2]),
4269 dst: node_offset(arena, node.id),
4270 m: m as u32,
4271 k: k as u32,
4272 n: n as u32,
4273 e5m2: false,
4274 },
4275 QuantScheme::Fp8E5m2 => Thunk::DequantMatMulFp8 {
4276 x: node_offset(arena, node.inputs[0]),
4277 w_q: node_offset(arena, node.inputs[1]),
4278 scale: node_offset(arena, node.inputs[2]),
4279 dst: node_offset(arena, node.id),
4280 m: m as u32,
4281 k: k as u32,
4282 n: n as u32,
4283 e5m2: true,
4284 },
4285 QuantScheme::Int8Block { block_size } => Thunk::DequantMatMul {
4286 x: node_offset(arena, node.inputs[0]),
4287 w_q: node_offset(arena, node.inputs[1]),
4288 scale: node_offset(arena, node.inputs[2]),
4289 zp: node_offset(arena, node.inputs[3]),
4290 dst: node_offset(arena, node.id),
4291 m: m as u32,
4292 k: k as u32,
4293 n: n as u32,
4294 block_size: *block_size,
4295 is_asymmetric: false,
4296 },
4297 QuantScheme::Int8BlockAsym { block_size } => Thunk::DequantMatMul {
4298 x: node_offset(arena, node.inputs[0]),
4299 w_q: node_offset(arena, node.inputs[1]),
4300 scale: node_offset(arena, node.inputs[2]),
4301 zp: node_offset(arena, node.inputs[3]),
4302 dst: node_offset(arena, node.id),
4303 m: m as u32,
4304 k: k as u32,
4305 n: n as u32,
4306 block_size: *block_size,
4307 is_asymmetric: true,
4308 },
4309 other => panic!(
4310 "DequantMatMul on CPU supports Int8/Int4/FP8/NVFP4 legacy or GGUF schemes; got {other}"
4311 ),
4312 }
4313 }
4314 }
4315
4316 Op::ScaledMatMul {
4317 lhs_format,
4318 rhs_format,
4319 scale_layout,
4320 has_bias,
4321 } => {
4322 let n = node.shape.dim(node.shape.rank() - 1).unwrap_static();
4324 let total = node.shape.num_elements().unwrap();
4325 let m = total / n.max(1);
4326 let lhs_total = graph.node(node.inputs[0]).shape.num_elements().unwrap();
4327 let k = lhs_total / m.max(1);
4328 Thunk::ScaledMatMul {
4329 lhs: node_offset(arena, node.inputs[0]),
4330 rhs: node_offset(arena, node.inputs[1]),
4331 lhs_scale: node_offset(arena, node.inputs[2]),
4332 rhs_scale: node_offset(arena, node.inputs[3]),
4333 bias: if *has_bias {
4334 node_offset(arena, node.inputs[4])
4335 } else {
4336 0
4337 },
4338 dst: node_offset(arena, node.id),
4339 m: m as u32,
4340 k: k as u32,
4341 n: n as u32,
4342 lhs_fmt: *lhs_format,
4343 rhs_fmt: *rhs_format,
4344 layout: *scale_layout,
4345 has_bias: *has_bias,
4346 }
4347 }
4348
4349 Op::ScaledQuantize {
4350 format,
4351 scale_layout,
4352 } => {
4353 let xs = &graph.node(node.inputs[0]).shape;
4354 let cols = xs.dim(xs.rank() - 1).unwrap_static();
4355 let rows = xs.num_elements().unwrap() / cols.max(1);
4356 Thunk::ScaledQuantize {
4357 x: node_offset(arena, node.inputs[0]),
4358 scale: node_offset(arena, node.inputs[1]),
4359 dst: node_offset(arena, node.id),
4360 rows: rows as u32,
4361 cols: cols as u32,
4362 fmt: *format,
4363 layout: *scale_layout,
4364 }
4365 }
4366
4367 Op::ScaledQuantScale {
4368 format,
4369 scale_layout,
4370 } => {
4371 let xs = &graph.node(node.inputs[0]).shape;
4372 let cols = xs.dim(xs.rank() - 1).unwrap_static();
4373 let rows = xs.num_elements().unwrap() / cols.max(1);
4374 Thunk::ScaledQuantScale {
4375 x: node_offset(arena, node.inputs[0]),
4376 dst: node_offset(arena, node.id),
4377 rows: rows as u32,
4378 cols: cols as u32,
4379 fmt: *format,
4380 layout: *scale_layout,
4381 }
4382 }
4383
4384 Op::ScaledDequantize {
4385 format,
4386 scale_layout,
4387 } => {
4388 let xs = &graph.node(node.inputs[0]).shape;
4389 let cols = xs.dim(xs.rank() - 1).unwrap_static();
4390 let rows = xs.num_elements().unwrap() / cols.max(1);
4391 Thunk::ScaledDequantize {
4392 codes: node_offset(arena, node.inputs[0]),
4393 scale: node_offset(arena, node.inputs[1]),
4394 dst: node_offset(arena, node.id),
4395 rows: rows as u32,
4396 cols: cols as u32,
4397 fmt: *format,
4398 layout: *scale_layout,
4399 }
4400 }
4401
4402 Op::LoraMatMul { scale } => {
4403 let n = node.shape.dim(node.shape.rank() - 1).unwrap_static();
4405 let total = node.shape.num_elements().unwrap();
4406 let m = total / n.max(1);
4407 let x_total = graph.node(node.inputs[0]).shape.num_elements().unwrap();
4408 let k = x_total / m.max(1);
4409 let a_total = graph.node(node.inputs[2]).shape.num_elements().unwrap();
4410 let r = a_total / k.max(1);
4411 Thunk::LoraMatMul {
4412 x: node_offset(arena, node.inputs[0]),
4413 w: node_offset(arena, node.inputs[1]),
4414 a: node_offset(arena, node.inputs[2]),
4415 b: node_offset(arena, node.inputs[3]),
4416 dst: node_offset(arena, node.id),
4417 m: m as u32,
4418 k: k as u32,
4419 n: n as u32,
4420 r: r as u32,
4421 scale: *scale,
4422 }
4423 }
4424
4425 Op::Sample {
4426 top_k,
4427 top_p,
4428 temperature,
4429 seed,
4430 } => {
4431 let in_shape = &graph.node(node.inputs[0]).shape;
4432 let (batch, vocab) = if in_shape.rank() >= 2 {
4434 (
4435 in_shape.dim(0).unwrap_static(),
4436 in_shape.dim(in_shape.rank() - 1).unwrap_static(),
4437 )
4438 } else {
4439 (1, in_shape.num_elements().unwrap_or(0))
4440 };
4441 Thunk::Sample {
4442 logits: node_offset(arena, node.inputs[0]),
4443 dst: node_offset(arena, node.id),
4444 batch: batch as u32,
4445 vocab: vocab as u32,
4446 top_k: *top_k as u32,
4447 top_p: *top_p,
4448 temperature: *temperature,
4449 seed: *seed,
4450 }
4451 }
4452
4453 Op::RngNormal {
4454 mean,
4455 scale,
4456 key,
4457 op_seed,
4458 } => Thunk::RngNormal {
4459 dst: node_offset(arena, node.id),
4460 len: node.shape.num_elements().unwrap_or(0) as u32,
4461 mean: *mean,
4462 scale: *scale,
4463 key: *key,
4464 op_seed: *op_seed,
4465 },
4466
4467 Op::RngUniform {
4468 low,
4469 high,
4470 key,
4471 op_seed,
4472 } => Thunk::RngUniform {
4473 dst: node_offset(arena, node.id),
4474 len: node.shape.num_elements().unwrap_or(0) as u32,
4475 low: *low,
4476 high: *high,
4477 key: *key,
4478 op_seed: *op_seed,
4479 },
4480
4481 Op::Cumsum { axis, exclusive } => {
4482 let rank = node.shape.rank();
4487 let ax = if *axis < 0 {
4488 (rank as i32 + axis) as usize
4489 } else {
4490 *axis as usize
4491 };
4492 assert_eq!(
4493 ax,
4494 rank - 1,
4495 "Cumsum only supports the last axis on CPU today"
4496 );
4497 let cols = node.shape.dim(ax).unwrap_static();
4498 let total = node.shape.num_elements().unwrap();
4499 Thunk::Cumsum {
4500 src: node_offset(arena, node.inputs[0]),
4501 dst: node_offset(arena, node.id),
4502 rows: (total / cols) as u32,
4503 cols: cols as u32,
4504 exclusive: *exclusive,
4505 }
4506 }
4507
4508 Op::Attention {
4509 num_heads,
4510 head_dim,
4511 mask_kind,
4512 score_scale,
4513 attn_logit_softcap: _,
4514 } => {
4515 let q_shape = &graph.node(node.inputs[0]).shape;
4521 let k_shape = &graph.node(node.inputs[1]).shape;
4522 let rank = q_shape.rank();
4523 let (batch, seq, kv_seq, bhsd) = if rank == 4 {
4524 let d1 = q_shape.dim(1).unwrap_static();
4525 let d2 = q_shape.dim(2).unwrap_static();
4526 if d1 == *num_heads {
4527 (
4529 q_shape.dim(0).unwrap_static(),
4530 d2,
4531 k_shape.dim(2).unwrap_static(),
4532 true,
4533 )
4534 } else {
4535 (
4537 q_shape.dim(0).unwrap_static(),
4538 d1,
4539 k_shape.dim(1).unwrap_static(),
4540 false,
4541 )
4542 }
4543 } else if rank >= 3 {
4544 (
4545 q_shape.dim(0).unwrap_static(),
4546 q_shape.dim(1).unwrap_static(),
4547 k_shape.dim(1).unwrap_static(),
4548 false,
4549 )
4550 } else {
4551 (
4552 1,
4553 q_shape.dim(0).unwrap_static(),
4554 k_shape.dim(0).unwrap_static(),
4555 false,
4556 )
4557 };
4558 let mask_off = if matches!(
4559 mask_kind,
4560 rlx_ir::op::MaskKind::Custom | rlx_ir::op::MaskKind::Bias
4561 ) {
4562 node_offset(arena, node.inputs[3])
4563 } else {
4564 0
4565 };
4566 let hs = (*num_heads * *head_dim) as u32;
4567 Thunk::Attention {
4568 q: node_offset(arena, node.inputs[0]),
4569 k: node_offset(arena, node.inputs[1]),
4570 v: node_offset(arena, node.inputs[2]),
4571 mask: mask_off,
4572 out: node_offset(arena, node.id),
4573 batch: batch as u32,
4574 seq: seq as u32,
4575 kv_seq: kv_seq as u32,
4576 heads: *num_heads as u32,
4577 head_dim: *head_dim as u32,
4578 mask_kind: *mask_kind,
4579 scale: score_scale.unwrap_or((*head_dim as f32).powf(-0.5)),
4580 q_row_stride: hs,
4584 k_row_stride: hs,
4585 v_row_stride: hs,
4586 bhsd,
4587 }
4588 }
4589
4590 Op::AttentionBackward {
4591 num_heads,
4592 head_dim,
4593 mask_kind,
4594 wrt,
4595 } => {
4596 let q_shape = &graph.node(node.inputs[0]).shape;
4597 let k_shape = &graph.node(node.inputs[1]).shape;
4598 let rank = q_shape.rank();
4599 let (batch, seq, kv_seq, bhsd) = if rank == 4 {
4600 let d1 = q_shape.dim(1).unwrap_static();
4601 let d2 = q_shape.dim(2).unwrap_static();
4602 if d1 == *num_heads {
4603 (
4604 q_shape.dim(0).unwrap_static(),
4605 d2,
4606 k_shape.dim(2).unwrap_static(),
4607 true,
4608 )
4609 } else {
4610 (
4611 q_shape.dim(0).unwrap_static(),
4612 d1,
4613 k_shape.dim(1).unwrap_static(),
4614 false,
4615 )
4616 }
4617 } else if rank >= 3 {
4618 (
4619 q_shape.dim(0).unwrap_static(),
4620 q_shape.dim(1).unwrap_static(),
4621 k_shape.dim(1).unwrap_static(),
4622 false,
4623 )
4624 } else {
4625 (
4626 1,
4627 q_shape.dim(0).unwrap_static(),
4628 k_shape.dim(0).unwrap_static(),
4629 false,
4630 )
4631 };
4632 let mask_off = if matches!(
4633 mask_kind,
4634 rlx_ir::op::MaskKind::Custom | rlx_ir::op::MaskKind::Bias
4635 ) {
4636 node_offset(arena, node.inputs[4])
4637 } else {
4638 0
4639 };
4640 Thunk::AttentionBackward {
4641 q: node_offset(arena, node.inputs[0]),
4642 k: node_offset(arena, node.inputs[1]),
4643 v: node_offset(arena, node.inputs[2]),
4644 dy: node_offset(arena, node.inputs[3]),
4645 mask: mask_off,
4646 out: node_offset(arena, node.id),
4647 batch: batch as u32,
4648 seq: seq as u32,
4649 kv_seq: kv_seq as u32,
4650 heads: *num_heads as u32,
4651 head_dim: *head_dim as u32,
4652 mask_kind: *mask_kind,
4653 wrt: *wrt,
4654 bhsd,
4655 }
4656 }
4657
4658 Op::FusedAttentionBlock {
4659 num_heads,
4660 head_dim,
4661 has_bias,
4662 has_rope,
4663 } => {
4664 let x_shape = &graph.node(node.inputs[0]).shape;
4665 let (batch, seq) = if x_shape.rank() >= 3 {
4666 (
4667 x_shape.dim(0).unwrap_static(),
4668 x_shape.dim(1).unwrap_static(),
4669 )
4670 } else {
4671 let total = x_shape.num_elements().unwrap();
4672 let s = x_shape.dim(x_shape.rank() - 2).unwrap_static();
4673 (total / (s * num_heads * head_dim), s)
4674 };
4675 let hs = (*num_heads * *head_dim) as u32;
4676 let mut idx = 4;
4678 let (qkv_b_off, out_b_off) = if *has_bias {
4679 let qb = node_offset(arena, node.inputs[idx]);
4680 let ob = node_offset(arena, node.inputs[idx + 1]);
4681 idx += 2;
4682 (qb, ob)
4683 } else {
4684 (0, 0)
4685 };
4686 let (cos_off, sin_off, cl) = if *has_rope {
4687 let c = node_offset(arena, node.inputs[idx]);
4688 let s = node_offset(arena, node.inputs[idx + 1]);
4689 let clen = get_len(graph, node.inputs[idx]);
4690 (c, s, clen as u32)
4691 } else {
4692 (0, 0, 0)
4693 };
4694
4695 Thunk::FusedAttnBlock {
4696 hidden: node_offset(arena, node.inputs[0]),
4697 qkv_w: node_offset(arena, node.inputs[1]),
4698 out_w: node_offset(arena, node.inputs[2]),
4699 mask: node_offset(arena, node.inputs[3]),
4700 mask_kind: rlx_ir::op::MaskKind::Custom,
4704 out: node_offset(arena, node.id),
4705 qkv_b: qkv_b_off,
4706 out_b: out_b_off,
4707 cos: cos_off,
4708 sin: sin_off,
4709 cos_len: cl,
4710 batch: batch as u32,
4711 seq: seq as u32,
4712 hs,
4713 nh: *num_heads as u32,
4714 dh: *head_dim as u32,
4715 has_bias: *has_bias,
4716 has_rope: *has_rope,
4717 interleaved: false,
4719 }
4720 }
4721
4722 Op::Rope {
4723 head_dim,
4724 n_rot,
4725 style,
4726 } => {
4727 let x_shape = &graph.node(node.inputs[0]).shape;
4728 let (batch, seq, hidden) = if x_shape.rank() >= 3 {
4729 (
4730 x_shape.dim(0).unwrap_static(),
4731 x_shape.dim(1).unwrap_static(),
4732 x_shape.dim(2).unwrap_static(),
4733 )
4734 } else {
4735 let total = x_shape.num_elements().unwrap();
4736 (
4737 1,
4738 x_shape.dim(0).unwrap_static(),
4739 total / x_shape.dim(0).unwrap_static(),
4740 )
4741 };
4742 let cos_len = get_len(graph, node.inputs[1]);
4743 Thunk::Rope {
4744 src: node_offset(arena, node.inputs[0]),
4745 cos: node_offset(arena, node.inputs[1]),
4746 sin: node_offset(arena, node.inputs[2]),
4747 dst: node_offset(arena, node.id),
4748 batch: batch as u32,
4749 seq: seq as u32,
4750 hidden: hidden as u32,
4751 head_dim: *head_dim as u32,
4752 n_rot: *n_rot as u32,
4753 cos_len: cos_len as u32,
4754 src_row_stride: hidden as u32,
4758 interleaved: matches!(style, rlx_ir::op::RopeStyle::GptJ),
4759 }
4760 }
4761
4762 Op::FusedSwiGLU {
4763 cast_to: _,
4764 gate_first,
4765 } => {
4766 let n_half = node.shape.dim(node.shape.rank() - 1).unwrap_static();
4767 let total = node.shape.num_elements().unwrap();
4768 Thunk::FusedSwiGLU {
4769 src: node_offset(arena, node.inputs[0]),
4770 dst: node_offset(arena, node.id),
4771 n_half: n_half as u32,
4772 total: total as u32,
4773 gate_first: *gate_first,
4774 }
4775 }
4776
4777 Op::Conv {
4778 kernel_size,
4779 stride,
4780 padding,
4781 dilation,
4782 groups,
4783 } => {
4784 let in_shape = &graph.node(node.inputs[0]).shape;
4785 let w_shape = &graph.node(node.inputs[1]).shape;
4786 let out_shape = &node.shape;
4787 let is_1x1_simple = kernel_size.len() == 2
4791 && kernel_size[0] == 1
4792 && kernel_size[1] == 1
4793 && stride.iter().all(|&s| s == 1)
4794 && padding.iter().all(|&p| p == 0)
4795 && dilation.iter().all(|&d| d == 1)
4796 && *groups == 1;
4797 if is_1x1_simple
4798 && in_shape.rank() >= 3
4799 && out_shape.rank() >= 3
4800 && w_shape.rank() >= 2
4801 {
4802 let (n, c_in, h, w) = conv_nchw_dims(in_shape);
4803 let (_, c_out, _, _) = conv_nchw_dims(out_shape);
4804 Thunk::Conv2D1x1 {
4805 src: node_offset(arena, node.inputs[0]),
4806 weight: node_offset(arena, node.inputs[1]),
4807 dst: node_offset(arena, node.id),
4808 n,
4809 c_in,
4810 c_out,
4811 hw: h.saturating_mul(w),
4812 }
4813 } else if kernel_size.len() == 2
4814 && in_shape.rank() >= 3
4815 && w_shape.rank() >= 2
4816 && out_shape.rank() >= 3
4817 {
4818 let (n, c_in, h, w_in) = conv_nchw_dims(in_shape);
4819 let (_, c_out, h_out, w_out) = conv_nchw_dims(out_shape);
4820 let one_d_w = h == 1
4828 && w_in > 1
4829 && kernel_size[0] > 1
4830 && kernel_size.get(1).copied().unwrap_or(1) == 1;
4831 let (h, w_in, h_out, w_out, kh, kw, sh, sw, ph, pw, dh, dw) = if one_d_w {
4832 (
4833 w_in,
4834 1,
4835 w_out,
4836 1,
4837 kernel_size[0] as u32,
4838 1,
4839 stride.first().copied().unwrap_or(1) as u32,
4840 1,
4841 padding.first().copied().unwrap_or(0) as u32,
4842 0,
4843 dilation.first().copied().unwrap_or(1) as u32,
4844 1,
4845 )
4846 } else {
4847 (
4848 h,
4849 w_in,
4850 h_out,
4851 w_out,
4852 kernel_size[0] as u32,
4853 kernel_size[1] as u32,
4854 stride.first().copied().unwrap_or(1) as u32,
4855 stride.get(1).copied().unwrap_or(1) as u32,
4856 padding.first().copied().unwrap_or(0) as u32,
4857 padding.get(1).copied().unwrap_or(0) as u32,
4858 dilation.first().copied().unwrap_or(1) as u32,
4859 dilation.get(1).copied().unwrap_or(1) as u32,
4860 )
4861 };
4862 Thunk::Conv2D {
4863 src: node_offset(arena, node.inputs[0]),
4864 weight: node_offset(arena, node.inputs[1]),
4865 dst: node_offset(arena, node.id),
4866 n,
4867 c_in,
4868 h,
4869 w: w_in,
4870 c_out,
4871 h_out,
4872 w_out,
4873 kh,
4874 kw,
4875 sh,
4876 sw,
4877 ph,
4878 pw,
4879 dh,
4880 dw,
4881 groups: *groups as u32,
4882 }
4883 } else {
4884 Thunk::Nop
4885 }
4886 }
4887
4888 Op::Pool {
4889 kind,
4890 kernel_size,
4891 stride,
4892 padding,
4893 } => {
4894 let in_shape = &graph.node(node.inputs[0]).shape;
4896 let out_shape = &node.shape;
4897 if kernel_size.len() == 2 && in_shape.rank() == 4 && out_shape.rank() == 4 {
4898 Thunk::Pool2D {
4899 src: node_offset(arena, node.inputs[0]),
4900 dst: node_offset(arena, node.id),
4901 n: in_shape.dim(0).unwrap_static() as u32,
4902 c: in_shape.dim(1).unwrap_static() as u32,
4903 h: in_shape.dim(2).unwrap_static() as u32,
4904 w: in_shape.dim(3).unwrap_static() as u32,
4905 h_out: out_shape.dim(2).unwrap_static() as u32,
4906 w_out: out_shape.dim(3).unwrap_static() as u32,
4907 kh: kernel_size[0] as u32,
4908 kw: kernel_size[1] as u32,
4909 sh: stride.first().copied().unwrap_or(1) as u32,
4910 sw: stride.get(1).copied().unwrap_or(1) as u32,
4911 ph: padding.first().copied().unwrap_or(0) as u32,
4912 pw: padding.get(1).copied().unwrap_or(0) as u32,
4913 kind: *kind,
4914 }
4915 } else {
4916 Thunk::Nop
4917 }
4918 }
4919
4920 Op::Transpose { perm } => {
4921 let in_shape = &graph.node(node.inputs[0]).shape;
4924 let in_rank = in_shape.rank();
4925 if perm.iter().any(|&p| p >= in_rank) {
4926 Thunk::Nop
4927 } else {
4928 let in_dims: Vec<usize> = (0..in_rank)
4929 .map(|i| in_shape.dim(i).unwrap_static())
4930 .collect();
4931 let mut in_strides_full = vec![1usize; in_rank];
4933 for d in (0..in_rank.saturating_sub(1)).rev() {
4934 in_strides_full[d] = in_strides_full[d + 1] * in_dims[d + 1];
4935 }
4936 let out_dims: Vec<u32> = perm.iter().map(|&p| in_dims[p] as u32).collect();
4937 let in_strides: Vec<u32> =
4938 perm.iter().map(|&p| in_strides_full[p] as u32).collect();
4939 let in_total = in_dims.iter().product::<usize>() as u32;
4940 let src = node_offset(arena, node.inputs[0]);
4941 let dst = node_offset(arena, node.id);
4942 let elem_bytes = node.shape.dtype().size_bytes() as u8;
4943 match node.shape.dtype() {
4944 rlx_ir::DType::F64 => Thunk::TransposeF64 {
4945 src,
4946 dst,
4947 in_total,
4948 out_dims,
4949 in_strides,
4950 },
4951 _ => Thunk::Transpose {
4952 src,
4953 dst,
4954 in_total,
4955 out_dims,
4956 in_strides,
4957 elem_bytes,
4958 },
4959 }
4960 }
4961 }
4962
4963 Op::ScatterAdd => {
4964 let upd_shape = &graph.node(node.inputs[0]).shape;
4967 let out_shape = &node.shape;
4968 let num_updates = upd_shape.dim(0).unwrap_static();
4969 let out_dim = out_shape.dim(0).unwrap_static();
4970 let trailing: usize = (1..out_shape.rank())
4971 .map(|i| out_shape.dim(i).unwrap_static())
4972 .product::<usize>()
4973 .max(1);
4974 Thunk::ScatterAdd {
4975 updates: node_offset(arena, node.inputs[0]),
4976 indices: node_offset(arena, node.inputs[1]),
4977 dst: node_offset(arena, node.id),
4978 num_updates: num_updates as u32,
4979 out_dim: out_dim as u32,
4980 trailing: trailing as u32,
4981 }
4982 }
4983
4984 Op::GroupedMatMul => {
4985 let in_shape = &graph.node(node.inputs[0]).shape;
4987 let w_shape = &graph.node(node.inputs[1]).shape;
4988 let m = in_shape.dim(in_shape.rank() - 2).unwrap_static();
4989 let k_dim = in_shape.dim(in_shape.rank() - 1).unwrap_static();
4990 let num_experts = w_shape.dim(0).unwrap_static();
4991 let n = w_shape.dim(2).unwrap_static();
4992 Thunk::GroupedMatMul {
4993 input: node_offset(arena, node.inputs[0]),
4994 weight: node_offset(arena, node.inputs[1]),
4995 expert_idx: node_offset(arena, node.inputs[2]),
4996 dst: node_offset(arena, node.id),
4997 m: m as u32,
4998 k_dim: k_dim as u32,
4999 n: n as u32,
5000 num_experts: num_experts as u32,
5001 }
5002 }
5003
5004 Op::DequantGroupedMatMul { scheme } => {
5005 let in_shape = &graph.node(node.inputs[0]).shape;
5006 let w_shape = &graph.node(node.inputs[1]).shape;
5007 let m = in_shape.dim(in_shape.rank() - 2).unwrap_static();
5008 let k_dim = in_shape.dim(in_shape.rank() - 1).unwrap_static();
5009 let out_shape = &node.shape;
5010 let n = out_shape.dim(out_shape.rank() - 1).unwrap_static();
5011 let block_elems = scheme.gguf_block_size() as usize;
5012 let block_bytes = scheme.gguf_block_bytes() as usize;
5013 let slab_bytes = (k_dim * n) / block_elems * block_bytes;
5014 let total_bytes = w_shape.num_elements().unwrap();
5015 let num_experts = total_bytes / slab_bytes.max(1);
5016 Thunk::DequantGroupedMatMulGguf {
5017 input: node_offset(arena, node.inputs[0]),
5018 w_q: node_offset(arena, node.inputs[1]),
5019 expert_idx: node_offset(arena, node.inputs[2]),
5020 dst: node_offset(arena, node.id),
5021 m: m as u32,
5022 k_dim: k_dim as u32,
5023 n: n as u32,
5024 num_experts: num_experts as u32,
5025 scheme: *scheme,
5026 }
5027 }
5028
5029 Op::DequantMoEWeights { scheme } => {
5030 let w_shape = &graph.node(node.inputs[0]).shape;
5031 let out_shape = &node.shape;
5032 let num_experts = out_shape.dim(0).unwrap_static();
5033 let k_dim = out_shape.dim(1).unwrap_static();
5034 let n = out_shape.dim(2).unwrap_static();
5035 let block_elems = scheme.gguf_block_size() as usize;
5036 let block_bytes = scheme.gguf_block_bytes() as usize;
5037 let slab_bytes = (k_dim * n) / block_elems * block_bytes;
5038 let total_bytes = w_shape.num_elements().unwrap();
5039 assert_eq!(
5040 total_bytes,
5041 num_experts * slab_bytes,
5042 "DequantMoEWeights packed bytes mismatch"
5043 );
5044 Thunk::DequantMoEWeightsGguf {
5045 w_q: node_offset(arena, node.inputs[0]),
5046 dst: node_offset(arena, node.id),
5047 k_dim: k_dim as u32,
5048 n: n as u32,
5049 num_experts: num_experts as u32,
5050 scheme: *scheme,
5051 }
5052 }
5053
5054 Op::TopK { k } => {
5055 let in_shape = &graph.node(node.inputs[0]).shape;
5056 let rank = in_shape.rank();
5057 let axis_dim = in_shape.dim(rank - 1).unwrap_static();
5058 let outer = in_shape.num_elements().unwrap() / axis_dim;
5059 let indices_i64 = u8::from(graph.node(node.id).shape.dtype() == rlx_ir::DType::I64);
5060 Thunk::TopK {
5061 src: node_offset(arena, node.inputs[0]),
5062 dst: node_offset(arena, node.id),
5063 outer: outer as u32,
5064 axis_dim: axis_dim as u32,
5065 k: *k as u32,
5066 indices_i64,
5067 }
5068 }
5069
5070 Op::Reduce {
5071 op,
5072 axes,
5073 keep_dim: _,
5074 } => {
5075 let in_shape = &graph.node(node.inputs[0]).shape;
5081 let rank = in_shape.rank();
5082 let mut sorted = axes.clone();
5083 sorted.sort();
5084 sorted.dedup();
5085 let contiguous = sorted.windows(2).all(|w| w[1] == w[0] + 1)
5086 && !sorted.is_empty()
5087 && *sorted.last().unwrap() < rank;
5088 if !contiguous {
5089 Thunk::Nop
5090 } else {
5091 let first = sorted[0];
5092 let last = *sorted.last().unwrap();
5093 let outer: usize = (0..first)
5094 .map(|i| in_shape.dim(i).unwrap_static())
5095 .product::<usize>()
5096 .max(1);
5097 let reduced: usize = (first..=last)
5098 .map(|i| in_shape.dim(i).unwrap_static())
5099 .product();
5100 let inner: usize = (last + 1..rank)
5101 .map(|i| in_shape.dim(i).unwrap_static())
5102 .product::<usize>()
5103 .max(1);
5104 let src = node_offset(arena, node.inputs[0]);
5105 let dst = node_offset(arena, node.id);
5106 if node.shape.dtype() == rlx_ir::DType::F64 && matches!(op, ReduceOp::Sum) {
5107 Thunk::ReduceSumF64 {
5108 src,
5109 dst,
5110 outer: outer as u32,
5111 reduced: reduced as u32,
5112 inner: inner as u32,
5113 }
5114 } else {
5115 Thunk::Reduce {
5116 src,
5117 dst,
5118 outer: outer as u32,
5119 reduced: reduced as u32,
5120 inner: inner as u32,
5121 op: *op,
5122 }
5123 }
5124 }
5125 }
5126
5127 Op::ArgMax { axis, keep_dim: _ } | Op::ArgMin { axis, keep_dim: _ } => {
5128 let in_shape = &graph.node(node.inputs[0]).shape;
5129 let rank = in_shape.rank();
5130 let outer: usize = (0..*axis)
5131 .map(|i| in_shape.dim(i).unwrap_static())
5132 .product::<usize>()
5133 .max(1);
5134 let reduced = in_shape.dim(*axis).unwrap_static();
5135 let inner: usize = (*axis + 1..rank)
5136 .map(|i| in_shape.dim(i).unwrap_static())
5137 .product::<usize>()
5138 .max(1);
5139 Thunk::ArgReduce {
5140 src: node_offset(arena, node.inputs[0]),
5141 dst: node_offset(arena, node.id),
5142 outer: outer as u32,
5143 reduced: reduced as u32,
5144 inner: inner as u32,
5145 is_max: matches!(node.op, Op::ArgMax { .. }),
5146 }
5147 }
5148
5149 Op::Compare(cmp) => {
5150 let len = node.shape.num_elements().unwrap();
5151 let in_dtype = graph.node(node.inputs[0]).shape.dtype();
5152 let inputs_i64 = u8::from(in_dtype == rlx_ir::DType::I64);
5153 Thunk::Compare {
5154 lhs: node_offset(arena, node.inputs[0]),
5155 rhs: node_offset(arena, node.inputs[1]),
5156 dst: node_offset(arena, node.id),
5157 len: len as u32,
5158 op: *cmp,
5159 inputs_i64,
5160 inputs_elem_bytes: in_dtype.size_bytes() as u8,
5161 dst_elem_bytes: node.shape.dtype().size_bytes() as u8,
5162 }
5163 }
5164
5165 Op::Where => {
5166 let len = node.shape.num_elements().unwrap();
5167 let elem_bytes = node.shape.dtype().size_bytes() as u8;
5168 let cond_elem_bytes = graph.node(node.inputs[0]).shape.dtype().size_bytes() as u8;
5169 Thunk::Where {
5170 cond: node_offset(arena, node.inputs[0]),
5171 on_true: node_offset(arena, node.inputs[1]),
5172 on_false: node_offset(arena, node.inputs[2]),
5173 dst: node_offset(arena, node.id),
5174 len: len as u32,
5175 elem_bytes,
5176 cond_elem_bytes,
5177 }
5178 }
5179
5180 Op::Fma => {
5181 let len = node.shape.num_elements().unwrap();
5182 Thunk::Fma {
5183 a: node_offset(arena, node.inputs[0]),
5184 b: node_offset(arena, node.inputs[1]),
5185 c: node_offset(arena, node.inputs[2]),
5186 dst: node_offset(arena, node.id),
5187 len: len as u32,
5188 elem_bytes: node.shape.dtype().size_bytes() as u8,
5189 }
5190 }
5191
5192 Op::ReluBackward => {
5193 let len: usize = (0..node.shape.rank())
5194 .map(|i| node.shape.dim(i).unwrap_static())
5195 .product();
5196 let x = node_offset(arena, node.inputs[0]);
5197 let dy = node_offset(arena, node.inputs[1]);
5198 let dx = node_offset(arena, node.id);
5199 match node.shape.dtype() {
5200 rlx_ir::DType::F64 => Thunk::ReluBackwardF64 {
5201 x,
5202 dy,
5203 dx,
5204 len: len as u32,
5205 },
5206 _ => Thunk::ReluBackward {
5207 x,
5208 dy,
5209 dx,
5210 len: len as u32,
5211 },
5212 }
5213 }
5214
5215 Op::ComplexNormSq => {
5216 let len: usize = (0..node.shape.rank())
5217 .map(|i| node.shape.dim(i).unwrap_static())
5218 .product();
5219 let src = node_offset(arena, node.inputs[0]);
5220 let dst = node_offset(arena, node.id);
5221 Thunk::ComplexNormSqF32 {
5222 src,
5223 dst,
5224 len: len as u32,
5225 }
5226 }
5227
5228 Op::ComplexNormSqBackward => {
5229 let len: usize = (0..node.shape.rank())
5230 .map(|i| node.shape.dim(i).unwrap_static())
5231 .product();
5232 let z = node_offset(arena, node.inputs[0]);
5233 let g = node_offset(arena, node.inputs[1]);
5234 let dz = node_offset(arena, node.id);
5235 Thunk::ComplexNormSqBackwardF32 {
5236 z,
5237 g,
5238 dz,
5239 len: len as u32,
5240 }
5241 }
5242
5243 Op::Conjugate => {
5244 let len: usize = (0..node.shape.rank())
5245 .map(|i| node.shape.dim(i).unwrap_static())
5246 .product();
5247 Thunk::ConjugateC64 {
5248 src: node_offset(arena, node.inputs[0]),
5249 dst: node_offset(arena, node.id),
5250 len: len as u32,
5251 }
5252 }
5253
5254 Op::ActivationBackward { kind } => {
5255 let len: usize = (0..node.shape.rank())
5256 .map(|i| node.shape.dim(i).unwrap_static())
5257 .product();
5258 let x = node_offset(arena, node.inputs[0]);
5259 let dy = node_offset(arena, node.inputs[1]);
5260 let dx = node_offset(arena, node.id);
5261 match node.shape.dtype() {
5262 rlx_ir::DType::F64 => Thunk::ActivationBackwardF64 {
5263 x,
5264 dy,
5265 dx,
5266 len: len as u32,
5267 kind: *kind,
5268 },
5269 _ => Thunk::ActivationBackward {
5270 x,
5271 dy,
5272 dx,
5273 len: len as u32,
5274 kind: *kind,
5275 },
5276 }
5277 }
5278
5279 Op::LayerNormBackwardInput { eps, .. } => {
5280 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
5282 let total = node.shape.num_elements().unwrap();
5283 Thunk::LayerNormBackwardInput {
5284 x: node_offset(arena, node.inputs[0]),
5285 gamma: node_offset(arena, node.inputs[1]),
5286 dy: node_offset(arena, node.inputs[2]),
5287 dx: node_offset(arena, node.id),
5288 rows: (total / h) as u32,
5289 h: h as u32,
5290 eps: *eps,
5291 }
5292 }
5293
5294 Op::LayerNormBackwardGamma { eps, .. } => {
5295 let x_shape = &graph.node(node.inputs[0]).shape;
5296 let h = x_shape.dim(x_shape.rank() - 1).unwrap_static();
5297 let x_total = x_shape.num_elements().unwrap();
5298 Thunk::LayerNormBackwardGamma {
5299 x: node_offset(arena, node.inputs[0]),
5300 dy: node_offset(arena, node.inputs[1]),
5301 dgamma: node_offset(arena, node.id),
5302 rows: (x_total / h) as u32,
5303 h: h as u32,
5304 eps: *eps,
5305 }
5306 }
5307
5308 Op::RmsNormBackwardInput { eps, .. }
5309 | Op::RmsNormBackwardGamma { eps, .. }
5310 | Op::RmsNormBackwardBeta { eps, .. } => {
5311 let x_shape = &graph.node(node.inputs[0]).shape;
5312 let h = x_shape.dim(x_shape.rank() - 1).unwrap_static();
5313 let rows = (x_shape.num_elements().unwrap() / h) as u32;
5314 let off = |i: usize| node_offset(arena, node.inputs[i]);
5315 let common = (off(0), off(1), off(2), off(3), rows, h as u32, *eps);
5316 match &node.op {
5317 Op::RmsNormBackwardInput { .. } => Thunk::RmsNormBackwardInput {
5318 x: common.0,
5319 gamma: common.1,
5320 beta: common.2,
5321 dy: common.3,
5322 dx: node_offset(arena, node.id),
5323 rows: common.4,
5324 h: common.5,
5325 eps: common.6,
5326 },
5327 Op::RmsNormBackwardGamma { .. } => Thunk::RmsNormBackwardGamma {
5328 x: common.0,
5329 gamma: common.1,
5330 beta: common.2,
5331 dy: common.3,
5332 dgamma: node_offset(arena, node.id),
5333 rows: common.4,
5334 h: common.5,
5335 eps: common.6,
5336 },
5337 Op::RmsNormBackwardBeta { .. } => Thunk::RmsNormBackwardBeta {
5338 x: common.0,
5339 gamma: common.1,
5340 beta: common.2,
5341 dy: common.3,
5342 dbeta: node_offset(arena, node.id),
5343 rows: common.4,
5344 h: common.5,
5345 eps: common.6,
5346 },
5347 _ => unreachable!(),
5348 }
5349 }
5350
5351 Op::RopeBackward { head_dim, n_rot } => {
5352 let dy_shape = &graph.node(node.inputs[0]).shape;
5353 let (batch, seq, hidden) = if dy_shape.rank() >= 3 {
5354 (
5355 dy_shape.dim(0).unwrap_static(),
5356 dy_shape.dim(1).unwrap_static(),
5357 dy_shape.dim(2).unwrap_static(),
5358 )
5359 } else {
5360 (
5361 1,
5362 dy_shape.dim(0).unwrap_static(),
5363 dy_shape.dim(1).unwrap_static(),
5364 )
5365 };
5366 let cos_shape = &graph.node(node.inputs[1]).shape;
5367 let cos_len = cos_shape.num_elements().unwrap();
5368 Thunk::RopeBackward {
5369 dy: node_offset(arena, node.inputs[0]),
5370 cos: node_offset(arena, node.inputs[1]),
5371 sin: node_offset(arena, node.inputs[2]),
5372 dx: node_offset(arena, node.id),
5373 batch: batch as u32,
5374 seq: seq as u32,
5375 hidden: hidden as u32,
5376 head_dim: *head_dim as u32,
5377 n_rot: *n_rot as u32,
5378 cos_len: cos_len as u32,
5379 }
5380 }
5381
5382 Op::CumsumBackward { exclusive, .. } => {
5383 let dy_shape = &graph.node(node.inputs[0]).shape;
5384 let rank = dy_shape.rank();
5385 let cols = dy_shape.dim(rank - 1).unwrap_static();
5386 let rows = dy_shape.num_elements().unwrap() / cols;
5387 Thunk::CumsumBackward {
5388 dy: node_offset(arena, node.inputs[0]),
5389 dx: node_offset(arena, node.id),
5390 rows: rows as u32,
5391 cols: cols as u32,
5392 exclusive: *exclusive,
5393 }
5394 }
5395
5396 Op::GatherBackward { .. } => {
5397 let dy_shape = &graph.node(node.inputs[0]).shape;
5398 let idx_shape = &graph.node(node.inputs[1]).shape;
5399 let out_shape = &node.shape;
5400 let rank = out_shape.rank();
5401 let axis = match &node.op {
5402 Op::GatherBackward { axis } => *axis,
5403 _ => 0,
5404 };
5405 let axis_u = if axis < 0 {
5406 (rank as i32 + axis) as usize
5407 } else {
5408 axis as usize
5409 };
5410 let outer: usize = (0..axis_u)
5411 .map(|i| dy_shape.dim(i).unwrap_static())
5412 .product::<usize>()
5413 .max(1);
5414 let num_idx = idx_shape.dim(axis_u).unwrap_static();
5415 let trailing: usize = (axis_u + 1..dy_shape.rank())
5416 .map(|i| dy_shape.dim(i).unwrap_static())
5417 .product::<usize>()
5418 .max(1);
5419 let axis_dim = out_shape.dim(axis_u).unwrap_static();
5420 Thunk::GatherBackward {
5421 dy: node_offset(arena, node.inputs[0]),
5422 indices: node_offset(arena, node.inputs[1]),
5423 dst: node_offset(arena, node.id),
5424 outer: outer as u32,
5425 axis_dim: axis_dim as u32,
5426 num_idx: num_idx as u32,
5427 trailing: trailing as u32,
5428 }
5429 }
5430
5431 Op::GroupNormBackwardInput { num_groups, eps }
5432 | Op::GroupNormBackwardGamma { num_groups, eps }
5433 | Op::GroupNormBackwardBeta { num_groups, eps } => {
5434 let x_shape = &graph.node(node.inputs[0]).shape;
5435 let n = x_shape.dim(0).unwrap_static() as u32;
5436 let c = x_shape.dim(1).unwrap_static() as u32;
5437 let h = x_shape.dim(2).unwrap_static() as u32;
5438 let w = x_shape.dim(3).unwrap_static() as u32;
5439 match &node.op {
5440 Op::GroupNormBackwardInput { .. } => Thunk::GroupNormBackwardInput {
5441 x: node_offset(arena, node.inputs[0]),
5442 gamma: node_offset(arena, node.inputs[1]),
5443 beta: node_offset(arena, node.inputs[2]),
5444 dy: node_offset(arena, node.inputs[3]),
5445 dx: node_offset(arena, node.id),
5446 n,
5447 c,
5448 h,
5449 w,
5450 num_groups: *num_groups as u32,
5451 eps: *eps,
5452 },
5453 Op::GroupNormBackwardGamma { .. } => Thunk::GroupNormBackwardGamma {
5454 x: node_offset(arena, node.inputs[0]),
5455 dy: node_offset(arena, node.inputs[1]),
5456 dgamma: node_offset(arena, node.id),
5457 n,
5458 c,
5459 h,
5460 w,
5461 num_groups: *num_groups as u32,
5462 eps: *eps,
5463 },
5464 Op::GroupNormBackwardBeta { .. } => Thunk::GroupNormBackwardBeta {
5465 dy: node_offset(arena, node.inputs[1]),
5466 dbeta: node_offset(arena, node.id),
5467 n,
5468 c,
5469 h,
5470 w,
5471 },
5472 _ => unreachable!(),
5473 }
5474 }
5475
5476 Op::MaxPool2dBackward {
5477 kernel_size,
5478 stride,
5479 padding,
5480 } => {
5481 let x_shape = &graph.node(node.inputs[0]).shape;
5482 let dy_shape = &graph.node(node.inputs[1]).shape;
5483 if kernel_size.len() == 2 && x_shape.rank() == 4 && dy_shape.rank() == 4 {
5484 Thunk::MaxPool2dBackward {
5485 x: node_offset(arena, node.inputs[0]),
5486 dy: node_offset(arena, node.inputs[1]),
5487 dx: node_offset(arena, node.id),
5488 n: x_shape.dim(0).unwrap_static() as u32,
5489 c: x_shape.dim(1).unwrap_static() as u32,
5490 h: x_shape.dim(2).unwrap_static() as u32,
5491 w: x_shape.dim(3).unwrap_static() as u32,
5492 h_out: dy_shape.dim(2).unwrap_static() as u32,
5493 w_out: dy_shape.dim(3).unwrap_static() as u32,
5494 kh: kernel_size[0] as u32,
5495 kw: kernel_size[1] as u32,
5496 sh: stride.first().copied().unwrap_or(1) as u32,
5497 sw: stride.get(1).copied().unwrap_or(1) as u32,
5498 ph: padding.first().copied().unwrap_or(0) as u32,
5499 pw: padding.get(1).copied().unwrap_or(0) as u32,
5500 }
5501 } else {
5502 Thunk::Nop
5503 }
5504 }
5505
5506 Op::Conv2dBackwardInput {
5507 kernel_size,
5508 stride,
5509 padding,
5510 dilation,
5511 groups,
5512 } => {
5513 let dy_shape = &graph.node(node.inputs[0]).shape;
5514 let w_shape = &graph.node(node.inputs[1]).shape;
5515 let out_shape = &node.shape;
5516 if kernel_size.len() == 2
5517 && dy_shape.rank() == 4
5518 && w_shape.rank() == 4
5519 && out_shape.rank() == 4
5520 {
5521 Thunk::Conv2dBackwardInput {
5522 dy: node_offset(arena, node.inputs[0]),
5523 w: node_offset(arena, node.inputs[1]),
5524 dx: node_offset(arena, node.id),
5525 n: out_shape.dim(0).unwrap_static() as u32,
5526 c_in: out_shape.dim(1).unwrap_static() as u32,
5527 h: out_shape.dim(2).unwrap_static() as u32,
5528 w_in: out_shape.dim(3).unwrap_static() as u32,
5529 c_out: dy_shape.dim(1).unwrap_static() as u32,
5530 h_out: dy_shape.dim(2).unwrap_static() as u32,
5531 w_out: dy_shape.dim(3).unwrap_static() as u32,
5532 kh: kernel_size[0] as u32,
5533 kw: kernel_size[1] as u32,
5534 sh: stride.first().copied().unwrap_or(1) as u32,
5535 sw: stride.get(1).copied().unwrap_or(1) as u32,
5536 ph: padding.first().copied().unwrap_or(0) as u32,
5537 pw: padding.get(1).copied().unwrap_or(0) as u32,
5538 dh: dilation.first().copied().unwrap_or(1) as u32,
5539 dw: dilation.get(1).copied().unwrap_or(1) as u32,
5540 groups: *groups as u32,
5541 }
5542 } else {
5543 Thunk::Nop
5544 }
5545 }
5546
5547 Op::Conv2dBackwardWeight {
5548 kernel_size,
5549 stride,
5550 padding,
5551 dilation,
5552 groups,
5553 } => {
5554 let x_shape = &graph.node(node.inputs[0]).shape;
5555 let dy_shape = &graph.node(node.inputs[1]).shape;
5556 let dw_shape = &node.shape;
5557 if kernel_size.len() == 2
5558 && x_shape.rank() == 4
5559 && dy_shape.rank() == 4
5560 && dw_shape.rank() == 4
5561 {
5562 Thunk::Conv2dBackwardWeight {
5563 x: node_offset(arena, node.inputs[0]),
5564 dy: node_offset(arena, node.inputs[1]),
5565 dw: node_offset(arena, node.id),
5566 n: x_shape.dim(0).unwrap_static() as u32,
5567 c_in: x_shape.dim(1).unwrap_static() as u32,
5568 h: x_shape.dim(2).unwrap_static() as u32,
5569 w: x_shape.dim(3).unwrap_static() as u32,
5570 c_out: dy_shape.dim(1).unwrap_static() as u32,
5571 h_out: dy_shape.dim(2).unwrap_static() as u32,
5572 w_out: dy_shape.dim(3).unwrap_static() as u32,
5573 kh: kernel_size[0] as u32,
5574 kw: kernel_size[1] as u32,
5575 sh: stride.first().copied().unwrap_or(1) as u32,
5576 sw: stride.get(1).copied().unwrap_or(1) as u32,
5577 ph: padding.first().copied().unwrap_or(0) as u32,
5578 pw: padding.get(1).copied().unwrap_or(0) as u32,
5579 dh: dilation.first().copied().unwrap_or(1) as u32,
5580 dw_dil: dilation.get(1).copied().unwrap_or(1) as u32,
5581 groups: *groups as u32,
5582 }
5583 } else {
5584 Thunk::Nop
5585 }
5586 }
5587
5588 Op::Im2Col {
5589 kernel_size,
5590 stride,
5591 padding,
5592 dilation,
5593 } => {
5594 let x_shape = &graph.node(node.inputs[0]).shape;
5595 let out_shape = &node.shape;
5596 if kernel_size.len() == 2 && x_shape.rank() == 4 && out_shape.rank() == 2 {
5597 let n = match x_shape.dim(0) {
5598 rlx_ir::shape::Dim::Static(v) => v as u32,
5599 _ => 0,
5600 };
5601 let c_in = x_shape.dim(1).unwrap_static() as u32;
5602 let h = x_shape.dim(2).unwrap_static() as u32;
5603 let w = x_shape.dim(3).unwrap_static() as u32;
5604 let kh = kernel_size[0] as u32;
5605 let kw = kernel_size[1] as u32;
5606 let sh = stride.first().copied().unwrap_or(1) as u32;
5607 let sw = stride.get(1).copied().unwrap_or(1) as u32;
5608 let ph = padding.first().copied().unwrap_or(0) as u32;
5609 let pw = padding.get(1).copied().unwrap_or(0) as u32;
5610 let dh = dilation.first().copied().unwrap_or(1) as u32;
5611 let dw_dil = dilation.get(1).copied().unwrap_or(1) as u32;
5612 let h_out = rlx_ir::shape::conv2d_spatial_output(
5613 h as usize,
5614 kh as usize,
5615 sh as usize,
5616 ph as usize,
5617 dh as usize,
5618 ) as u32;
5619 let w_out = rlx_ir::shape::conv2d_spatial_output(
5620 w as usize,
5621 kw as usize,
5622 sw as usize,
5623 pw as usize,
5624 dw_dil as usize,
5625 ) as u32;
5626 Thunk::Im2Col {
5627 x: node_offset(arena, node.inputs[0]),
5628 col: node_offset(arena, node.id),
5629 n,
5630 c_in,
5631 h,
5632 w,
5633 h_out,
5634 w_out,
5635 kh,
5636 kw,
5637 sh,
5638 sw,
5639 ph,
5640 pw,
5641 dh,
5642 dw_dil,
5643 }
5644 } else {
5645 Thunk::Nop
5646 }
5647 }
5648
5649 Op::SoftmaxCrossEntropy => {
5650 let logits_shape = &graph.node(node.inputs[0]).shape;
5651 if logits_shape.rank() == 2 {
5652 Thunk::SoftmaxCrossEntropyDense {
5653 logits: node_offset(arena, node.inputs[0]),
5654 targets: node_offset(arena, node.inputs[1]),
5655 dst: node_offset(arena, node.id),
5656 n: logits_shape.dim(0).unwrap_static() as u32,
5657 c: logits_shape.dim(1).unwrap_static() as u32,
5658 }
5659 } else {
5660 Thunk::Nop
5661 }
5662 }
5663
5664 Op::SoftmaxCrossEntropyWithLogits => {
5665 let logits_shape = &graph.node(node.inputs[0]).shape;
5666 if logits_shape.rank() == 2 {
5667 Thunk::SoftmaxCrossEntropy {
5668 logits: node_offset(arena, node.inputs[0]),
5669 labels: node_offset(arena, node.inputs[1]),
5670 dst: node_offset(arena, node.id),
5671 n: logits_shape.dim(0).unwrap_static() as u32,
5672 c: logits_shape.dim(1).unwrap_static() as u32,
5673 }
5674 } else {
5675 Thunk::Nop
5676 }
5677 }
5678
5679 Op::SoftmaxCrossEntropyBackward => {
5680 let logits_shape = &graph.node(node.inputs[0]).shape;
5681 if logits_shape.rank() == 2 {
5682 Thunk::SoftmaxCrossEntropyBackward {
5683 logits: node_offset(arena, node.inputs[0]),
5684 labels: node_offset(arena, node.inputs[1]),
5685 d_loss: node_offset(arena, node.inputs[2]),
5686 dlogits: node_offset(arena, node.id),
5687 n: logits_shape.dim(0).unwrap_static() as u32,
5688 c: logits_shape.dim(1).unwrap_static() as u32,
5689 }
5690 } else {
5691 Thunk::Nop
5692 }
5693 }
5694
5695 Op::DenseSolve => {
5696 let a_shape = &graph.node(node.inputs[0]).shape;
5698 let n = a_shape.dim(0).unwrap_static();
5699 debug_assert_eq!(
5700 n,
5701 a_shape.dim(1).unwrap_static(),
5702 "DenseSolve: A must be square"
5703 );
5704 let b_elems = node.shape.num_elements().unwrap();
5705 let nrhs = b_elems / n;
5706 match node.shape.dtype() {
5707 rlx_ir::DType::F64 => Thunk::DenseSolveF64 {
5708 a: node_offset(arena, node.inputs[0]),
5709 b: node_offset(arena, node.inputs[1]),
5710 x: node_offset(arena, node.id),
5711 n: n as u32,
5712 nrhs: nrhs as u32,
5713 },
5714 rlx_ir::DType::F32 => Thunk::DenseSolveF32 {
5715 a: node_offset(arena, node.inputs[0]),
5716 b: node_offset(arena, node.inputs[1]),
5717 x: node_offset(arena, node.id),
5718 n: n as u32,
5719 nrhs: nrhs as u32,
5720 },
5721 other => panic!(
5722 "DenseSolve: F32 + F64 lowered; got {other:?}. \
5723 Add another variant when needed."
5724 ),
5725 }
5726 }
5727
5728 Op::BatchedDenseSolve => {
5729 let a_shape = &graph.node(node.inputs[0]).shape;
5731 assert_eq!(a_shape.rank(), 3, "BatchedDenseSolve: A rank must be 3");
5732 let batch = a_shape.dim(0).unwrap_static();
5733 let n = a_shape.dim(1).unwrap_static();
5734 debug_assert_eq!(
5735 n,
5736 a_shape.dim(2).unwrap_static(),
5737 "BatchedDenseSolve: A's last two dims must match"
5738 );
5739 let total = node.shape.num_elements().unwrap();
5740 let nrhs = total / (batch * n);
5741 match node.shape.dtype() {
5742 rlx_ir::DType::F32 => Thunk::BatchedDenseSolveF32 {
5743 a: node_offset(arena, node.inputs[0]),
5744 b: node_offset(arena, node.inputs[1]),
5745 x: node_offset(arena, node.id),
5746 batch: batch as u32,
5747 n: n as u32,
5748 nrhs: nrhs as u32,
5749 },
5750 rlx_ir::DType::F64 => Thunk::BatchedDenseSolveF64 {
5751 a: node_offset(arena, node.inputs[0]),
5752 b: node_offset(arena, node.inputs[1]),
5753 x: node_offset(arena, node.id),
5754 batch: batch as u32,
5755 n: n as u32,
5756 nrhs: nrhs as u32,
5757 },
5758 other => panic!("BatchedDenseSolve: F32 + F64 only, got {other:?}"),
5759 }
5760 }
5761
5762 Op::Scan {
5763 body,
5764 length,
5765 save_trajectory,
5766 num_bcast,
5767 num_xs,
5768 num_checkpoints,
5769 } => {
5770 assert!(
5771 *num_checkpoints == 0 || *num_checkpoints <= *length,
5772 "Op::Scan: num_checkpoints={} must be 0 or ≤ length={}",
5773 *num_checkpoints,
5774 *length
5775 );
5776 if *num_checkpoints != 0 && *num_checkpoints != *length {
5777 assert!(
5778 *save_trajectory,
5779 "Op::Scan: num_checkpoints<length only meaningful when save_trajectory=true"
5780 );
5781 }
5782 let body_plan = rlx_opt::memory::plan_memory(body);
5793 let _body_arena_size = body_plan.arena_size;
5794 let body_offsets: HashMap<NodeId, usize> = body_plan
5797 .assignments
5798 .iter()
5799 .map(|(id, slot)| (*id, slot.offset))
5800 .collect();
5801
5802 let mut body_inputs: Vec<NodeId> = body
5805 .nodes()
5806 .iter()
5807 .filter(|n| matches!(n.op, Op::Input { .. }))
5808 .map(|n| n.id)
5809 .collect();
5810 body_inputs.sort();
5811 let n_body_inputs = body_inputs.len();
5812 let expected = 1 + *num_bcast as usize + *num_xs as usize;
5813 if n_body_inputs != expected {
5814 let names: Vec<String> = body
5815 .nodes()
5816 .iter()
5817 .filter_map(|n| match &n.op {
5818 Op::Input { name } => Some(format!("{}={}", n.id, name)),
5819 _ => None,
5820 })
5821 .collect();
5822 panic!(
5823 "Op::Scan body has {} Op::Input nodes; expected {} \
5824 (1 carry + {} bcast + {} xs). Inputs by NodeId: [{}]",
5825 n_body_inputs,
5826 expected,
5827 *num_bcast,
5828 *num_xs,
5829 names.join(", ")
5830 );
5831 }
5832
5833 let body_input_id = body_inputs[0];
5834 let body_input_off = body_offsets[&body_input_id];
5835 let body_output_id = body
5836 .outputs
5837 .first()
5838 .copied()
5839 .expect("Op::Scan body must declare one output");
5840 let body_output_off = body_offsets[&body_output_id];
5841
5842 let mut body_arena = crate::arena::Arena::from_plan(body_plan);
5843 for n in body.nodes() {
5846 if let Op::Constant { data } = &n.op
5847 && body_arena.has_buffer(n.id)
5848 && !data.is_empty()
5849 {
5850 match n.shape.dtype() {
5851 rlx_ir::DType::F64 => {
5852 let off = body_arena.byte_offset(n.id);
5853 let buf = body_arena.raw_buf_mut();
5854 let nbytes = (buf.len() - off).min(data.len());
5855 buf[off..off + nbytes].copy_from_slice(&data[..nbytes]);
5856 }
5857 _ => {
5858 let buf = body_arena.slice_mut(n.id);
5859 let n_floats = data.len() / 4;
5860 let n_lim = buf.len().min(n_floats);
5861 for i in 0..n_lim {
5862 let bytes = [
5863 data[i * 4],
5864 data[i * 4 + 1],
5865 data[i * 4 + 2],
5866 data[i * 4 + 3],
5867 ];
5868 buf[i] = f32::from_le_bytes(bytes);
5869 }
5870 }
5871 }
5872 }
5873 }
5874 let body_init = body_arena.raw_buf().to_vec();
5875 let body_schedule = compile_thunks_with_rng(body, &body_arena, rng);
5876
5877 let carry_bytes = if *save_trajectory {
5882 let total = node
5883 .shape
5884 .size_bytes()
5885 .expect("Op::Scan trajectory output must have static shape");
5886 total / *length as usize
5887 } else {
5888 node.shape
5889 .size_bytes()
5890 .expect("Op::Scan carry must have static shape")
5891 };
5892
5893 let mut bcast_inputs: Vec<(usize, usize, u32)> =
5898 Vec::with_capacity(*num_bcast as usize);
5899 for i in 0..*num_bcast as usize {
5900 let body_b_id = body_inputs[1 + i];
5901 let body_b_off = body_offsets[&body_b_id];
5902 let outer_b_id = node.inputs[1 + i];
5903 let outer_b_off = node_offset(arena, outer_b_id);
5904 let outer_b_shape = &graph.node(outer_b_id).shape;
5905 let total = outer_b_shape
5906 .size_bytes()
5907 .expect("Op::Scan bcast must have static shape");
5908 bcast_inputs.push((body_b_off, outer_b_off, total as u32));
5909 }
5910
5911 let mut xs_inputs: Vec<(usize, usize, u32)> = Vec::with_capacity(*num_xs as usize);
5915 let xs_base = 1 + *num_bcast as usize;
5916 for i in 0..*num_xs as usize {
5917 let body_x_id = body_inputs[xs_base + i];
5918 let body_x_off = body_offsets[&body_x_id];
5919 let outer_xs_id = node.inputs[xs_base + i];
5920 let outer_xs_off = node_offset(arena, outer_xs_id);
5921 let outer_xs_shape = &graph.node(outer_xs_id).shape;
5922 let total = outer_xs_shape
5923 .size_bytes()
5924 .expect("Op::Scan xs must have static shape");
5925 let per_step = total / *length as usize;
5926 xs_inputs.push((body_x_off, outer_xs_off, per_step as u32));
5927 }
5928
5929 Thunk::Scan {
5930 body: Arc::new(body_schedule),
5931 body_init: Arc::new(body_init),
5932 body_input_off,
5933 body_output_off,
5934 outer_init_off: node_offset(arena, node.inputs[0]),
5935 outer_final_off: node_offset(arena, node.id),
5936 length: *length,
5937 carry_bytes: carry_bytes as u32,
5938 save_trajectory: *save_trajectory,
5939 xs_inputs: Arc::new(xs_inputs),
5940 bcast_inputs: Arc::new(bcast_inputs),
5941 num_checkpoints: *num_checkpoints,
5942 }
5943 }
5944
5945 Op::ScanBackward {
5946 body_vjp,
5947 length,
5948 save_trajectory,
5949 num_xs,
5950 num_checkpoints,
5951 forward_body,
5952 } => {
5953 let is_recursive = *num_checkpoints != 0 && *num_checkpoints != *length;
5954 if is_recursive {
5955 assert!(
5956 forward_body.is_some(),
5957 "Op::ScanBackward with num_checkpoints<length requires forward_body"
5958 );
5959 }
5960 let body_plan = rlx_opt::memory::plan_memory(body_vjp);
5968 let body_offsets: HashMap<NodeId, usize> = body_plan
5969 .assignments
5970 .iter()
5971 .map(|(id, slot)| (*id, slot.offset))
5972 .collect();
5973 let mut body_d_output_off: Option<usize> = None;
5974 let mut body_other_inputs: Vec<(NodeId, usize)> = Vec::new();
5975 for n in body_vjp.nodes() {
5976 if let Op::Input { name } = &n.op {
5977 let off = body_offsets[&n.id];
5978 if name == "d_output" {
5979 body_d_output_off = Some(off);
5980 } else {
5981 body_other_inputs.push((n.id, off));
5982 }
5983 }
5984 }
5985 body_other_inputs.sort_by_key(|(id, _)| *id);
5986 let body_d_output_off =
5987 body_d_output_off.expect("ScanBackward body_vjp missing 'd_output' Input");
5988 let expected_others = 1 + *num_xs as usize;
5989 assert_eq!(
5990 body_other_inputs.len(),
5991 expected_others,
5992 "ScanBackward body_vjp has {} non-d_output Inputs; \
5993 expected {} (1 carry + {} xs)",
5994 body_other_inputs.len(),
5995 expected_others,
5996 num_xs
5997 );
5998 let body_carry_in_off = body_other_inputs[0].1;
5999 let body_x_offs: Vec<usize> = body_other_inputs
6000 .iter()
6001 .skip(1)
6002 .map(|(_, off)| *off)
6003 .collect();
6004 let body_dcarry_out_off = body_offsets[&body_vjp.outputs[0]];
6005
6006 let mut body_arena = crate::arena::Arena::from_plan(body_plan);
6007 for n in body_vjp.nodes() {
6009 if let Op::Constant { data } = &n.op
6010 && body_arena.has_buffer(n.id)
6011 && !data.is_empty()
6012 {
6013 match n.shape.dtype() {
6014 rlx_ir::DType::F64 => {
6015 let off = body_arena.byte_offset(n.id);
6016 let buf = body_arena.raw_buf_mut();
6017 let nb = (buf.len() - off).min(data.len());
6018 buf[off..off + nb].copy_from_slice(&data[..nb]);
6019 }
6020 _ => {
6021 let buf = body_arena.slice_mut(n.id);
6022 let nf = data.len() / 4;
6023 let nl = buf.len().min(nf);
6024 for i in 0..nl {
6025 let bytes = [
6026 data[i * 4],
6027 data[i * 4 + 1],
6028 data[i * 4 + 2],
6029 data[i * 4 + 3],
6030 ];
6031 buf[i] = f32::from_le_bytes(bytes);
6032 }
6033 }
6034 }
6035 }
6036 }
6037 let body_init = body_arena.raw_buf().to_vec();
6038 let body_schedule = compile_thunks_with_rng(body_vjp, &body_arena, rng);
6039
6040 let carry_bytes = body_vjp
6042 .node(body_vjp.outputs[0])
6043 .shape
6044 .size_bytes()
6045 .expect("ScanBackward dcarry must be statically shaped");
6046 let carry_elem_size = body_vjp
6047 .node(body_vjp.outputs[0])
6048 .shape
6049 .dtype()
6050 .size_bytes() as u32;
6051
6052 let mut outer_xs_offs: Vec<(usize, u32)> = Vec::with_capacity(*num_xs as usize);
6055 for i in 0..*num_xs as usize {
6056 let outer_xs_id = node.inputs[3 + i];
6057 let outer_xs_off = node_offset(arena, outer_xs_id);
6058 let outer_xs_shape = &graph.node(outer_xs_id).shape;
6059 let total = outer_xs_shape
6060 .size_bytes()
6061 .expect("ScanBackward xs must have static shape");
6062 let per_step = total / *length as usize;
6063 outer_xs_offs.push((outer_xs_off, per_step as u32));
6064 }
6065
6066 let (fb_schedule, fb_init, fb_carry_in_off, fb_output_off, fb_x_offs) =
6071 if is_recursive {
6072 let fb = forward_body.as_ref().unwrap();
6073 let fb_plan = rlx_opt::memory::plan_memory(fb);
6074 let fb_offsets: HashMap<NodeId, usize> = fb_plan
6075 .assignments
6076 .iter()
6077 .map(|(id, slot)| (*id, slot.offset))
6078 .collect();
6079 let mut fb_inputs: Vec<NodeId> = fb
6080 .nodes()
6081 .iter()
6082 .filter(|n| matches!(n.op, Op::Input { .. }))
6083 .map(|n| n.id)
6084 .collect();
6085 fb_inputs.sort();
6086 let fb_carry = fb_offsets[&fb_inputs[0]];
6087 let fb_xs: Vec<usize> = (1..fb_inputs.len())
6088 .map(|i| fb_offsets[&fb_inputs[i]])
6089 .collect();
6090 let fb_out = fb_offsets[&fb.outputs[0]];
6091 let mut fb_arena = crate::arena::Arena::from_plan(fb_plan);
6092 for n in fb.nodes() {
6093 if let Op::Constant { data } = &n.op
6094 && fb_arena.has_buffer(n.id)
6095 && !data.is_empty()
6096 {
6097 let off = fb_arena.byte_offset(n.id);
6104 let buf = fb_arena.raw_buf_mut();
6105 let nb = (buf.len() - off).min(data.len());
6106 buf[off..off + nb].copy_from_slice(&data[..nb]);
6107 }
6108 }
6109 let fb_init_bytes = fb_arena.raw_buf().to_vec();
6110 let fb_sched = compile_thunks_with_rng(fb, &fb_arena, rng);
6111 (
6112 Some(Arc::new(fb_sched)),
6113 Some(Arc::new(fb_init_bytes)),
6114 fb_carry,
6115 fb_out,
6116 fb_xs,
6117 )
6118 } else {
6119 (None, None, 0, 0, Vec::new())
6120 };
6121
6122 Thunk::ScanBackward {
6123 body_vjp: Arc::new(body_schedule),
6124 body_init: Arc::new(body_init),
6125 body_carry_in_off,
6126 body_x_offs: Arc::new(body_x_offs),
6127 body_d_output_off,
6128 body_dcarry_out_off,
6129 outer_init_off: node_offset(arena, node.inputs[0]),
6130 outer_traj_off: node_offset(arena, node.inputs[1]),
6131 outer_upstream_off: node_offset(arena, node.inputs[2]),
6132 outer_xs_offs: Arc::new(outer_xs_offs),
6133 outer_dinit_off: node_offset(arena, node.id),
6134 length: *length,
6135 carry_bytes: carry_bytes as u32,
6136 carry_elem_size,
6137 save_trajectory: *save_trajectory,
6138 num_checkpoints: *num_checkpoints,
6139 forward_body: fb_schedule,
6140 forward_body_init: fb_init,
6141 forward_body_carry_in_off: fb_carry_in_off,
6142 forward_body_output_off: fb_output_off,
6143 forward_body_x_offs: Arc::new(fb_x_offs),
6144 }
6145 }
6146
6147 Op::ScanBackwardXs {
6148 body_vjp,
6149 length,
6150 save_trajectory,
6151 num_xs,
6152 xs_idx,
6153 num_checkpoints,
6154 forward_body,
6155 } => {
6156 assert!(
6157 *num_checkpoints == 0 || *num_checkpoints <= *length,
6158 "Op::ScanBackwardXs: num_checkpoints={} must be 0 or ≤ length={}",
6159 *num_checkpoints,
6160 *length
6161 );
6162 let is_recursive = *num_checkpoints != 0 && *num_checkpoints != *length;
6163 if is_recursive {
6164 assert!(
6165 forward_body.is_some(),
6166 "Op::ScanBackwardXs with num_checkpoints<length \
6167 requires forward_body"
6168 );
6169 }
6170 let body_plan = rlx_opt::memory::plan_memory(body_vjp);
6178 let body_offsets: HashMap<NodeId, usize> = body_plan
6179 .assignments
6180 .iter()
6181 .map(|(id, slot)| (*id, slot.offset))
6182 .collect();
6183 let mut body_d_output_off: Option<usize> = None;
6184 let mut body_other_inputs: Vec<(NodeId, usize)> = Vec::new();
6185 for n in body_vjp.nodes() {
6186 if let Op::Input { name } = &n.op {
6187 let off = body_offsets[&n.id];
6188 if name == "d_output" {
6189 body_d_output_off = Some(off);
6190 } else {
6191 body_other_inputs.push((n.id, off));
6192 }
6193 }
6194 }
6195 body_other_inputs.sort_by_key(|(id, _)| *id);
6196 let body_d_output_off =
6197 body_d_output_off.expect("ScanBackwardXs body_vjp missing 'd_output' Input");
6198 let expected_others = 1 + *num_xs as usize;
6199 assert_eq!(
6200 body_other_inputs.len(),
6201 expected_others,
6202 "ScanBackwardXs body_vjp has {} non-d_output Inputs; expected {}",
6203 body_other_inputs.len(),
6204 expected_others
6205 );
6206 let body_carry_in_off = body_other_inputs[0].1;
6207 let body_x_offs: Vec<usize> = body_other_inputs
6208 .iter()
6209 .skip(1)
6210 .map(|(_, off)| *off)
6211 .collect();
6212 let body_dcarry_out_off = body_offsets[&body_vjp.outputs[0]];
6213 let dxs_out_node = body_vjp.outputs[1 + *xs_idx as usize];
6214 let body_dxs_out_off = body_offsets[&dxs_out_node];
6215
6216 let mut body_arena = crate::arena::Arena::from_plan(body_plan);
6217 for n in body_vjp.nodes() {
6218 if let Op::Constant { data } = &n.op
6219 && body_arena.has_buffer(n.id)
6220 && !data.is_empty()
6221 {
6222 match n.shape.dtype() {
6223 rlx_ir::DType::F64 => {
6224 let off = body_arena.byte_offset(n.id);
6225 let buf = body_arena.raw_buf_mut();
6226 let nb = (buf.len() - off).min(data.len());
6227 buf[off..off + nb].copy_from_slice(&data[..nb]);
6228 }
6229 _ => {
6230 let buf = body_arena.slice_mut(n.id);
6231 let nf = data.len() / 4;
6232 let nl = buf.len().min(nf);
6233 for i in 0..nl {
6234 let bytes = [
6235 data[i * 4],
6236 data[i * 4 + 1],
6237 data[i * 4 + 2],
6238 data[i * 4 + 3],
6239 ];
6240 buf[i] = f32::from_le_bytes(bytes);
6241 }
6242 }
6243 }
6244 }
6245 }
6246 let body_init = body_arena.raw_buf().to_vec();
6247 let body_schedule = compile_thunks_with_rng(body_vjp, &body_arena, rng);
6248
6249 let carry_bytes = body_vjp
6250 .node(body_vjp.outputs[0])
6251 .shape
6252 .size_bytes()
6253 .expect("ScanBackwardXs dcarry must be statically shaped");
6254 let carry_elem_size = body_vjp
6255 .node(body_vjp.outputs[0])
6256 .shape
6257 .dtype()
6258 .size_bytes() as u32;
6259 let per_step_bytes = body_vjp
6260 .node(dxs_out_node)
6261 .shape
6262 .size_bytes()
6263 .expect("ScanBackwardXs dxs body output must be statically shaped");
6264
6265 let mut outer_xs_offs: Vec<(usize, u32)> = Vec::with_capacity(*num_xs as usize);
6266 for i in 0..*num_xs as usize {
6267 let outer_xs_id = node.inputs[3 + i];
6268 let outer_xs_off = node_offset(arena, outer_xs_id);
6269 let outer_xs_shape = &graph.node(outer_xs_id).shape;
6270 let total = outer_xs_shape
6271 .size_bytes()
6272 .expect("ScanBackwardXs xs must have static shape");
6273 let per_step = total / *length as usize;
6274 outer_xs_offs.push((outer_xs_off, per_step as u32));
6275 }
6276
6277 let (fb_schedule, fb_init, fb_carry_in_off, fb_output_off, fb_x_offs) =
6280 if is_recursive {
6281 let fb = forward_body.as_ref().unwrap();
6282 let fb_plan = rlx_opt::memory::plan_memory(fb);
6283 let fb_offsets: HashMap<NodeId, usize> = fb_plan
6284 .assignments
6285 .iter()
6286 .map(|(id, slot)| (*id, slot.offset))
6287 .collect();
6288 let mut fb_inputs: Vec<NodeId> = fb
6289 .nodes()
6290 .iter()
6291 .filter(|n| matches!(n.op, Op::Input { .. }))
6292 .map(|n| n.id)
6293 .collect();
6294 fb_inputs.sort();
6295 let fb_carry = fb_offsets[&fb_inputs[0]];
6296 let fb_xs: Vec<usize> = (1..fb_inputs.len())
6297 .map(|i| fb_offsets[&fb_inputs[i]])
6298 .collect();
6299 let fb_out = fb_offsets[&fb.outputs[0]];
6300 let mut fb_arena = crate::arena::Arena::from_plan(fb_plan);
6301 for n in fb.nodes() {
6302 if let Op::Constant { data } = &n.op
6303 && fb_arena.has_buffer(n.id)
6304 && !data.is_empty()
6305 {
6306 let off = fb_arena.byte_offset(n.id);
6313 let buf = fb_arena.raw_buf_mut();
6314 let nb = (buf.len() - off).min(data.len());
6315 buf[off..off + nb].copy_from_slice(&data[..nb]);
6316 }
6317 }
6318 let fb_init_bytes = fb_arena.raw_buf().to_vec();
6319 let fb_sched = compile_thunks_with_rng(fb, &fb_arena, rng);
6320 (
6321 Some(Arc::new(fb_sched)),
6322 Some(Arc::new(fb_init_bytes)),
6323 fb_carry,
6324 fb_out,
6325 fb_xs,
6326 )
6327 } else {
6328 (None, None, 0, 0, Vec::new())
6329 };
6330
6331 Thunk::ScanBackwardXs {
6332 body_vjp: Arc::new(body_schedule),
6333 body_init: Arc::new(body_init),
6334 body_carry_in_off,
6335 body_x_offs: Arc::new(body_x_offs),
6336 body_d_output_off,
6337 body_dcarry_out_off,
6338 body_dxs_out_off,
6339 outer_init_off: node_offset(arena, node.inputs[0]),
6340 outer_traj_off: node_offset(arena, node.inputs[1]),
6341 outer_upstream_off: node_offset(arena, node.inputs[2]),
6342 outer_xs_offs: Arc::new(outer_xs_offs),
6343 outer_dxs_off: node_offset(arena, node.id),
6344 length: *length,
6345 carry_bytes: carry_bytes as u32,
6346 carry_elem_size,
6347 per_step_bytes: per_step_bytes as u32,
6348 save_trajectory: *save_trajectory,
6349 num_checkpoints: *num_checkpoints,
6350 forward_body: fb_schedule,
6351 forward_body_init: fb_init,
6352 forward_body_carry_in_off: fb_carry_in_off,
6353 forward_body_output_off: fb_output_off,
6354 forward_body_x_offs: Arc::new(fb_x_offs),
6355 }
6356 }
6357
6358 Op::Concat { axis } => {
6359 let out_shape = &node.shape;
6363 let rank = out_shape.rank();
6364 let outer: usize = (0..*axis)
6365 .map(|i| out_shape.dim(i).unwrap_static())
6366 .product::<usize>()
6367 .max(1);
6368 let inner: usize = (*axis + 1..rank)
6369 .map(|i| out_shape.dim(i).unwrap_static())
6370 .product::<usize>()
6371 .max(1);
6372 let total_axis = out_shape.dim(*axis).unwrap_static();
6373 let inputs: Vec<(usize, u32, u32)> = node
6374 .inputs
6375 .iter()
6376 .map(|&in_id| {
6377 let in_shape = &graph.node(in_id).shape;
6378 let in_axis = concat_axis_extent(in_shape, *axis, rank);
6379 let in_numel = in_shape.num_elements().unwrap_or(0) as u32;
6380 (node_offset(arena, in_id), in_axis as u32, in_numel)
6381 })
6382 .collect();
6383 let dst = node_offset(arena, node.id);
6384 match out_shape.dtype() {
6385 rlx_ir::DType::F64 => Thunk::ConcatF64 {
6386 dst,
6387 outer: outer as u32,
6388 inner: inner as u32,
6389 total_axis: total_axis as u32,
6390 inputs,
6391 },
6392 _ => Thunk::Concat {
6393 dst,
6394 outer: outer as u32,
6395 inner: inner as u32,
6396 total_axis: total_axis as u32,
6397 inputs,
6398 },
6399 }
6400 }
6401
6402 Op::GaussianSplatRender {
6403 width,
6404 height,
6405 tile_size,
6406 radius_scale,
6407 alpha_cutoff,
6408 max_splat_steps,
6409 transmittance_threshold,
6410 max_list_entries,
6411 } => {
6412 let elem_len =
6413 |id: NodeId| -> usize { graph.node(id).shape.num_elements().unwrap_or(0) };
6414 Thunk::GaussianSplatRender {
6415 positions_off: node_offset(arena, node.inputs[0]),
6416 positions_len: elem_len(node.inputs[0]),
6417 scales_off: node_offset(arena, node.inputs[1]),
6418 scales_len: elem_len(node.inputs[1]),
6419 rotations_off: node_offset(arena, node.inputs[2]),
6420 rotations_len: elem_len(node.inputs[2]),
6421 opacities_off: node_offset(arena, node.inputs[3]),
6422 opacities_len: elem_len(node.inputs[3]),
6423 colors_off: node_offset(arena, node.inputs[4]),
6424 colors_len: elem_len(node.inputs[4]),
6425 sh_coeffs_off: node_offset(arena, node.inputs[5]),
6426 sh_coeffs_len: elem_len(node.inputs[5]),
6427 meta_off: node_offset(arena, node.inputs[6]),
6428 dst_off: node_offset(arena, node.id),
6429 dst_len: node.shape.num_elements().unwrap_or(0),
6430 width: *width,
6431 height: *height,
6432 tile_size: *tile_size,
6433 radius_scale: *radius_scale,
6434 alpha_cutoff: *alpha_cutoff,
6435 max_splat_steps: *max_splat_steps,
6436 transmittance_threshold: *transmittance_threshold,
6437 max_list_entries: *max_list_entries,
6438 }
6439 }
6440
6441 Op::GaussianSplatRenderBackward {
6442 width,
6443 height,
6444 tile_size,
6445 radius_scale,
6446 alpha_cutoff,
6447 max_splat_steps,
6448 transmittance_threshold,
6449 max_list_entries,
6450 loss_grad_clip,
6451 sh_band,
6452 max_anisotropy,
6453 } => {
6454 let elem_len =
6455 |id: NodeId| -> usize { graph.node(id).shape.num_elements().unwrap_or(0) };
6456 Thunk::GaussianSplatRenderBackward {
6457 positions_off: node_offset(arena, node.inputs[0]),
6458 positions_len: elem_len(node.inputs[0]),
6459 scales_off: node_offset(arena, node.inputs[1]),
6460 scales_len: elem_len(node.inputs[1]),
6461 rotations_off: node_offset(arena, node.inputs[2]),
6462 rotations_len: elem_len(node.inputs[2]),
6463 opacities_off: node_offset(arena, node.inputs[3]),
6464 opacities_len: elem_len(node.inputs[3]),
6465 colors_off: node_offset(arena, node.inputs[4]),
6466 colors_len: elem_len(node.inputs[4]),
6467 sh_coeffs_off: node_offset(arena, node.inputs[5]),
6468 sh_coeffs_len: elem_len(node.inputs[5]),
6469 meta_off: node_offset(arena, node.inputs[6]),
6470 d_loss_off: node_offset(arena, node.inputs[7]),
6471 d_loss_len: elem_len(node.inputs[7]),
6472 packed_off: node_offset(arena, node.id),
6473 packed_len: node.shape.num_elements().unwrap_or(0),
6474 width: *width,
6475 height: *height,
6476 tile_size: *tile_size,
6477 radius_scale: *radius_scale,
6478 alpha_cutoff: *alpha_cutoff,
6479 max_splat_steps: *max_splat_steps,
6480 transmittance_threshold: *transmittance_threshold,
6481 max_list_entries: *max_list_entries,
6482 loss_grad_clip: *loss_grad_clip,
6483 sh_band: *sh_band,
6484 max_anisotropy: *max_anisotropy,
6485 }
6486 }
6487
6488 Op::GaussianSplatPrepare {
6489 width,
6490 height,
6491 tile_size,
6492 radius_scale,
6493 alpha_cutoff,
6494 max_splat_steps,
6495 transmittance_threshold,
6496 max_list_entries,
6497 } => {
6498 let elem_len =
6499 |id: NodeId| -> usize { graph.node(id).shape.num_elements().unwrap_or(0) };
6500 Thunk::GaussianSplatPrepare {
6501 positions_off: node_offset(arena, node.inputs[0]),
6502 positions_len: elem_len(node.inputs[0]),
6503 scales_off: node_offset(arena, node.inputs[1]),
6504 scales_len: elem_len(node.inputs[1]),
6505 rotations_off: node_offset(arena, node.inputs[2]),
6506 rotations_len: elem_len(node.inputs[2]),
6507 opacities_off: node_offset(arena, node.inputs[3]),
6508 opacities_len: elem_len(node.inputs[3]),
6509 colors_off: node_offset(arena, node.inputs[4]),
6510 colors_len: elem_len(node.inputs[4]),
6511 sh_coeffs_off: node_offset(arena, node.inputs[5]),
6512 sh_coeffs_len: elem_len(node.inputs[5]),
6513 meta_off: node_offset(arena, node.inputs[6]),
6514 meta_len: elem_len(node.inputs[6]),
6515 prep_off: node_offset(arena, node.id),
6516 prep_len: node.shape.num_elements().unwrap_or(0),
6517 width: *width,
6518 height: *height,
6519 tile_size: *tile_size,
6520 radius_scale: *radius_scale,
6521 alpha_cutoff: *alpha_cutoff,
6522 max_splat_steps: *max_splat_steps,
6523 transmittance_threshold: *transmittance_threshold,
6524 max_list_entries: *max_list_entries,
6525 }
6526 }
6527
6528 Op::GaussianSplatRasterize {
6529 width,
6530 height,
6531 tile_size,
6532 alpha_cutoff,
6533 max_splat_steps,
6534 transmittance_threshold,
6535 max_list_entries,
6536 } => {
6537 let elem_len =
6538 |id: NodeId| -> usize { graph.node(id).shape.num_elements().unwrap_or(0) };
6539 let prep_id = node.inputs[0];
6540 let count = match &graph.node(prep_id).op {
6541 rlx_ir::Op::GaussianSplatPrepare { .. } => {
6542 elem_len(graph.node(prep_id).inputs[0]) / 3
6543 }
6544 _ => 1,
6545 };
6546 Thunk::GaussianSplatRasterize {
6547 prep_off: node_offset(arena, prep_id),
6548 prep_len: elem_len(prep_id),
6549 meta_off: node_offset(arena, node.inputs[1]),
6550 meta_len: elem_len(node.inputs[1]),
6551 dst_off: node_offset(arena, node.id),
6552 dst_len: node.shape.num_elements().unwrap_or(0),
6553 count,
6554 width: *width,
6555 height: *height,
6556 tile_size: *tile_size,
6557 alpha_cutoff: *alpha_cutoff,
6558 max_splat_steps: *max_splat_steps,
6559 transmittance_threshold: *transmittance_threshold,
6560 max_list_entries: *max_list_entries,
6561 }
6562 }
6563
6564 Op::Custom { name, attrs, .. } => {
6565 let kernel = crate::op_registry::lookup_cpu_kernel(name).unwrap_or_else(|| {
6566 panic!(
6567 "compile_thunks: no CPU kernel registered for \
6568 Op::Custom('{name}'). Register one via \
6569 rlx_cpu::op_registry::register_cpu_kernel \
6570 before compiling on the CPU backend."
6571 )
6572 });
6573 let inputs_v: Vec<(usize, u32, Shape)> = node
6574 .inputs
6575 .iter()
6576 .map(|&in_id| {
6577 let s = graph.node(in_id).shape.clone();
6578 let len = s.num_elements().unwrap_or(0) as u32;
6579 (node_offset(arena, in_id), len, s)
6580 })
6581 .collect();
6582 let out_len = node.shape.num_elements().unwrap_or(0) as u32;
6583 Thunk::CustomOp {
6584 kernel,
6585 inputs: inputs_v,
6586 output: (node_offset(arena, node.id), out_len, node.shape.clone()),
6587 attrs: attrs.clone(),
6588 }
6589 }
6590
6591 Op::Fft { inverse, norm } => {
6592 let shape = &node.shape;
6593 let meta = rlx_ir::fft::fft_meta(shape);
6594 let dtype = shape.dtype();
6595 assert!(
6596 matches!(
6597 dtype,
6598 rlx_ir::DType::F32 | rlx_ir::DType::F64 | rlx_ir::DType::C64
6599 ),
6600 "Op::Fft on CPU requires F32, F64, or C64, got {dtype:?}"
6601 );
6602 Thunk::Fft1d {
6603 src: node_offset(arena, node.inputs[0]),
6604 dst: node_offset(arena, node.id),
6605 outer: meta.outer as u32,
6606 n_complex: meta.n_complex as u32,
6607 inverse: *inverse,
6608 norm_tag: norm.tag(),
6609 dtype,
6610 }
6611 }
6612
6613 Op::FftButterflyStage { stage, n_fft } => {
6614 let state_shape = graph.node(node.inputs[0]).shape.clone();
6615 assert_eq!(
6616 state_shape.dtype(),
6617 rlx_ir::DType::F32,
6618 "Op::FftButterflyStage requires F32 state"
6619 );
6620 let batch = state_shape.dim(0).unwrap_static() as u32;
6621 Thunk::FftButterflyStage {
6622 state_src: node_offset(arena, node.inputs[0]),
6623 state_dst: node_offset(arena, node.id),
6624 gate_src: node_offset(arena, node.inputs[1]),
6625 rev_src: node_offset(arena, node.inputs[2]),
6626 tw_re_src: node_offset(arena, node.inputs[3]),
6627 tw_im_src: node_offset(arena, node.inputs[4]),
6628 batch,
6629 n_fft: *n_fft,
6630 stage: *stage,
6631 }
6632 }
6633
6634 Op::LogMel => {
6635 let spec_shape = graph.node(node.inputs[0]).shape.clone();
6636 let filt_shape = graph.node(node.inputs[1]).shape.clone();
6637 let meta = rlx_ir::audio::log_mel_meta(&spec_shape, &filt_shape)
6638 .unwrap_or_else(|e| panic!("Op::LogMel: {e}"));
6639 Thunk::LogMel {
6640 spec: node_offset(arena, node.inputs[0]),
6641 filters: node_offset(arena, node.inputs[1]),
6642 dst: node_offset(arena, node.id),
6643 outer: meta.outer as u32,
6644 n_fft: meta.n_fft as u32,
6645 n_bins: meta.n_bins as u32,
6646 n_mels: meta.n_mels as u32,
6647 }
6648 }
6649
6650 Op::LogMelBackward => {
6651 let spec_shape = graph.node(node.inputs[0]).shape.clone();
6652 let filt_shape = graph.node(node.inputs[1]).shape.clone();
6653 let meta = rlx_ir::audio::log_mel_meta(&spec_shape, &filt_shape)
6654 .unwrap_or_else(|e| panic!("Op::LogMelBackward: {e}"));
6655 Thunk::LogMelBackward {
6656 spec: node_offset(arena, node.inputs[0]),
6657 filters: node_offset(arena, node.inputs[1]),
6658 dy: node_offset(arena, node.inputs[2]),
6659 dst: node_offset(arena, node.id),
6660 outer: meta.outer as u32,
6661 n_fft: meta.n_fft as u32,
6662 n_bins: meta.n_bins as u32,
6663 n_mels: meta.n_mels as u32,
6664 }
6665 }
6666
6667 Op::WelchPeaks { k, n_segments } => {
6668 let spec_shape = graph.node(node.inputs[0]).shape.clone();
6669 let meta = rlx_ir::audio::welch_peaks_meta(&spec_shape, *k, *n_segments)
6670 .unwrap_or_else(|e| panic!("Op::WelchPeaks: {e}"));
6671 Thunk::WelchPeaks {
6672 spec: node_offset(arena, node.inputs[0]),
6673 dst: node_offset(arena, node.id),
6674 welch_batch: meta.welch_batch as u32,
6675 n_fft: meta.n_fft as u32,
6676 n_segments: meta.n_segments as u32,
6677 k: meta.k as u32,
6678 }
6679 }
6680
6681 Op::CustomFn {
6682 fwd_body,
6683 num_inputs,
6684 ..
6685 } => {
6686 let body_plan = rlx_opt::memory::plan_memory(fwd_body);
6692 let body_offsets: HashMap<NodeId, usize> = body_plan
6693 .assignments
6694 .iter()
6695 .map(|(id, slot)| (*id, slot.offset))
6696 .collect();
6697
6698 let mut body_input_ids: Vec<NodeId> = fwd_body
6699 .nodes()
6700 .iter()
6701 .filter(|n| matches!(n.op, Op::Input { .. }))
6702 .map(|n| n.id)
6703 .collect();
6704 body_input_ids.sort();
6705 assert_eq!(
6706 body_input_ids.len(),
6707 *num_inputs as usize,
6708 "Op::CustomFn fwd_body has {} Op::Input(s); declared num_inputs={}",
6709 body_input_ids.len(),
6710 *num_inputs,
6711 );
6712
6713 let mut body_arena = crate::arena::Arena::from_plan(body_plan);
6714 for n in fwd_body.nodes() {
6715 if let Op::Constant { data } = &n.op
6716 && body_arena.has_buffer(n.id)
6717 && !data.is_empty()
6718 {
6719 match n.shape.dtype() {
6720 rlx_ir::DType::F64 => {
6721 let off = body_arena.byte_offset(n.id);
6722 let buf = body_arena.raw_buf_mut();
6723 let nb = (buf.len() - off).min(data.len());
6724 buf[off..off + nb].copy_from_slice(&data[..nb]);
6725 }
6726 _ => {
6727 let buf = body_arena.slice_mut(n.id);
6728 let nf = data.len() / 4;
6729 let nl = buf.len().min(nf);
6730 for i in 0..nl {
6731 let bytes = [
6732 data[i * 4],
6733 data[i * 4 + 1],
6734 data[i * 4 + 2],
6735 data[i * 4 + 3],
6736 ];
6737 buf[i] = f32::from_le_bytes(bytes);
6738 }
6739 }
6740 }
6741 }
6742 }
6743 let body_init = body_arena.raw_buf().to_vec();
6744 let body_schedule = compile_thunks_with_rng(fwd_body, &body_arena, rng);
6745
6746 let inputs_v: Vec<(usize, usize, u32)> = (0..*num_inputs as usize)
6748 .map(|i| {
6749 let body_in = body_input_ids[i];
6750 let body_off = body_offsets[&body_in];
6751 let outer_in = node.inputs[i];
6752 let outer_off = node_offset(arena, outer_in);
6753 let bytes = graph
6754 .node(outer_in)
6755 .shape
6756 .size_bytes()
6757 .expect("Op::CustomFn primal input must have static shape");
6758 (body_off, outer_off, bytes as u32)
6759 })
6760 .collect();
6761
6762 let body_output_id = fwd_body
6763 .outputs
6764 .first()
6765 .copied()
6766 .expect("Op::CustomFn fwd_body must declare exactly one output");
6767 let body_output_off = body_offsets[&body_output_id];
6768 let out_bytes = node
6769 .shape
6770 .size_bytes()
6771 .expect("Op::CustomFn output must have static shape");
6772
6773 Thunk::CustomFn {
6774 body: Arc::new(body_schedule),
6775 body_init: Arc::new(body_init),
6776 inputs: Arc::new(inputs_v),
6777 body_output_off,
6778 outer_output_off: node_offset(arena, node.id),
6779 out_bytes: out_bytes as u32,
6780 }
6781 }
6782
6783 Op::ElementwiseRegion {
6784 chain,
6785 scalar_input_mask,
6786 input_modulus,
6787 prologue,
6788 ..
6789 } => {
6790 if *prologue != rlx_ir::op::RegionPrologue::None {
6794 Thunk::Nop
6795 } else {
6796 let input_offs: Vec<usize> = node
6797 .inputs
6798 .iter()
6799 .map(|&id| node_offset(arena, id))
6800 .collect();
6801 Thunk::ElementwiseRegion {
6802 dst: node_offset(arena, node.id),
6803 len: node.shape.num_elements().unwrap_or(0) as u32,
6804 input_offs,
6805 chain: chain.clone(),
6806 scalar_input_mask: *scalar_input_mask,
6807 input_modulus: *input_modulus,
6808 }
6809 }
6810 }
6811 _ => Thunk::Nop,
6812 };
6813 thunks.push(t);
6814 }
6815
6816 let cfg = crate::config::RuntimeConfig::global();
6817 let mask_thr = cfg.mask_binary_threshold;
6818 let mask_neg = cfg.attn_mask_neg_inf;
6819 let score_skip = cfg.score_skip_threshold;
6820
6821 let compiled_fns: Vec<Arc<dyn Fn(*mut u8) + Send + Sync>> = thunks
6823 .iter()
6824 .filter(|t| !matches!(t, Thunk::Nop))
6825 .map(|thunk| {
6826 match thunk.clone() {
6827 Thunk::Nop => Arc::new(|_: *mut u8| {}) as Arc<dyn Fn(*mut u8) + Send + Sync>,
6828
6829 Thunk::Sgemm { a, b, c, m, k, n } => {
6830 let (m, k, n) = (m as usize, k as usize, n as usize);
6831 Arc::new(move |base: *mut u8| unsafe {
6832 crate::blas::sgemm(
6833 sl(a, base, m * k),
6834 sl(b, base, k * n),
6835 sl_mut(c, base, m * n),
6836 m,
6837 k,
6838 n,
6839 );
6840 })
6841 }
6842
6843 Thunk::CgemmC64 { a, b, c, m, k, n } => {
6844 let (m, k, n) = (m as usize, k as usize, n as usize);
6845 Arc::new(move |base: *mut u8| unsafe {
6846 cgemm_c64(a, b, c, m, k, n, base);
6847 })
6848 }
6849
6850 Thunk::DenseSolveF64 { a, b, x, n, nrhs } => {
6851 let (n_, nrhs_) = (n as usize, nrhs as usize);
6852 Arc::new(move |base: *mut u8| unsafe {
6853 let a_src = sl_f64(a, base, n_ * n_);
6854 let b_src = sl_f64(b, base, n_ * nrhs_);
6855 let mut a_scratch: Vec<f64> = a_src.to_vec();
6856 let mut x_buf: Vec<f64> = b_src.to_vec();
6857 let info = crate::blas::dgesv(&mut a_scratch, &mut x_buf, n_, nrhs_);
6858 if info != 0 {
6859 panic!("DenseSolveF64: singular (info={info})");
6860 }
6861 sl_mut_f64(x, base, n_ * nrhs_).copy_from_slice(&x_buf);
6862 })
6863 }
6864
6865 Thunk::DenseSolveF32 { a, b, x, n, nrhs } => {
6866 let (n_, nrhs_) = (n as usize, nrhs as usize);
6867 Arc::new(move |base: *mut u8| unsafe {
6868 let a_src = sl(a, base, n_ * n_);
6869 let b_src = sl(b, base, n_ * nrhs_);
6870 let mut a_scratch: Vec<f32> = a_src.to_vec();
6871 let mut x_buf: Vec<f32> = b_src.to_vec();
6872 let info = crate::blas::sgesv(&mut a_scratch, &mut x_buf, n_, nrhs_);
6873 if info != 0 {
6874 panic!("DenseSolveF32: singular (info={info})");
6875 }
6876 sl_mut(x, base, n_ * nrhs_).copy_from_slice(&x_buf);
6877 })
6878 }
6879
6880 Thunk::FusedMmBiasAct {
6881 a,
6882 w,
6883 bias,
6884 c,
6885 m,
6886 k,
6887 n,
6888 act,
6889 } => {
6890 let (m, k, n) = (m as usize, k as usize, n as usize);
6891 Arc::new(move |base: *mut u8| unsafe {
6892 let out = sl_mut(c, base, m * n);
6893 crate::blas::sgemm(sl(a, base, m * k), sl(w, base, k * n), out, m, k, n);
6894 match act {
6902 Some(Activation::Gelu) => {
6903 crate::kernels::par_bias_gelu(out, sl(bias, base, n), m, n)
6904 }
6905 Some(other) => {
6906 crate::blas::bias_add(out, sl(bias, base, n), m, n);
6907 apply_activation_inplace(out, other);
6908 }
6909 None => crate::blas::bias_add(out, sl(bias, base, n), m, n),
6910 }
6911 })
6912 }
6913
6914 Thunk::FusedResidualLN {
6915 x,
6916 res,
6917 bias,
6918 g,
6919 b,
6920 out,
6921 rows,
6922 h,
6923 eps,
6924 has_bias,
6925 } => {
6926 let (rows, h) = (rows as usize, h as usize);
6927 Arc::new(move |base: *mut u8| unsafe {
6928 let zero = vec![0f32; h]; let bi = if has_bias { sl(bias, base, h) } else { &zero };
6930 let xp = sl(x, base, rows * h).as_ptr() as usize;
6931 let rp = sl(res, base, rows * h).as_ptr() as usize;
6932 let op = sl_mut(out, base, rows * h).as_mut_ptr() as usize;
6933 let bp = bi.as_ptr() as usize;
6934 let gp = sl(g, base, h).as_ptr() as usize;
6935 let bbp = sl(b, base, h).as_ptr() as usize;
6936 crate::pool::par_for(rows, 4, &|off, cnt| {
6937 let xs = std::slice::from_raw_parts(
6938 (xp as *const f32).add(off * h),
6939 cnt * h,
6940 );
6941 let rs = std::slice::from_raw_parts(
6942 (rp as *const f32).add(off * h),
6943 cnt * h,
6944 );
6945 let os = std::slice::from_raw_parts_mut(
6946 (op as *mut f32).add(off * h),
6947 cnt * h,
6948 );
6949 let bi = std::slice::from_raw_parts(bp as *const f32, h);
6950 let g = std::slice::from_raw_parts(gp as *const f32, h);
6951 let b = std::slice::from_raw_parts(bbp as *const f32, h);
6952 crate::kernels::residual_bias_layer_norm(
6953 xs, rs, bi, g, b, os, cnt, h, eps,
6954 );
6955 });
6956 })
6957 }
6958
6959 Thunk::BiasAdd {
6960 src,
6961 bias,
6962 dst,
6963 m,
6964 n,
6965 } => {
6966 let (m, n) = (m as usize, n as usize);
6967 let len = m * n;
6968 Arc::new(move |base: *mut u8| unsafe {
6969 let out = sl_mut(dst, base, len);
6970 if src != dst {
6971 let src_ptr = base.add(src) as *const f32;
6972 let dst_ptr = base.add(dst) as *mut f32;
6973 if src_ptr != dst_ptr {
6974 std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, len);
6975 }
6976 }
6977 crate::blas::bias_add(out, sl(bias, base, n), m, n);
6978 })
6979 }
6980
6981 Thunk::Gather {
6982 table,
6983 table_len,
6984 idx,
6985 dst,
6986 num_idx,
6987 trailing,
6988 idx_i64,
6989 table_bytes,
6990 } => {
6991 let (ni, tr, tl) = (num_idx as usize, trailing as usize, table_len as usize);
6992 let rows = tl / tr.max(1);
6993 let (idx_i64, table_bytes) = (idx_i64, table_bytes);
6994 Arc::new(move |base: *mut u8| unsafe {
6995 if table_bytes == 8 {
6996 let tab = sl_i64(table, base, tl);
6997 let out = sl_mut_i64(dst, base, ni * tr);
6998 if idx_i64 != 0 {
6999 let ids = sl_i64(idx, base, ni);
7000 for i in 0..ni {
7001 let row = ids[i].max(0) as usize;
7002 if row < rows {
7003 out[i * tr..(i + 1) * tr]
7004 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
7005 }
7006 }
7007 } else {
7008 let ids = sl(idx, base, ni);
7009 for i in 0..ni {
7010 let row = ids[i] as usize;
7011 if row < rows {
7012 out[i * tr..(i + 1) * tr]
7013 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
7014 }
7015 }
7016 }
7017 } else {
7018 let tab = sl(table, base, tl);
7019 let out = sl_mut(dst, base, ni * tr);
7020 if idx_i64 != 0 {
7021 let ids = sl_i64(idx, base, ni);
7022 for i in 0..ni {
7023 let row = ids[i].max(0) as usize;
7024 if row < rows {
7025 out[i * tr..(i + 1) * tr]
7026 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
7027 }
7028 }
7029 } else {
7030 let ids = sl(idx, base, ni);
7031 for i in 0..ni {
7032 let row = ids[i] as usize;
7033 if row < rows {
7034 out[i * tr..(i + 1) * tr]
7035 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
7036 }
7037 }
7038 }
7039 }
7040 })
7041 }
7042
7043 Thunk::Narrow {
7044 src,
7045 dst,
7046 outer,
7047 src_stride,
7048 dst_stride,
7049 inner,
7050 elem_bytes,
7051 } => {
7052 narrow_thunk_closure(src, dst, outer, src_stride, dst_stride, inner, elem_bytes)
7053 }
7054
7055 Thunk::Reverse {
7056 src,
7057 dst,
7058 dims,
7059 rev_mask,
7060 elem_bytes,
7061 } => {
7062 let eb = elem_bytes as usize;
7063 let rank = dims.len();
7064 let total: usize = dims.iter().map(|&d| d as usize).product::<usize>().max(1);
7065 let mut strides = vec![1usize; rank];
7067 for i in (0..rank.saturating_sub(1)).rev() {
7068 strides[i] = strides[i + 1] * dims[i + 1] as usize;
7069 }
7070 let dims_u: Vec<usize> = dims.iter().map(|&d| d as usize).collect();
7071 Arc::new(move |base: *mut u8| unsafe {
7072 let src_base = base.add(src);
7073 let dst_base = base.add(dst);
7074 for o in 0..total {
7075 let mut rem = o;
7078 let mut in_flat = 0usize;
7079 for ax in 0..rank {
7080 let idx = rem / strides[ax];
7081 rem %= strides[ax];
7082 let in_idx = if rev_mask[ax] {
7083 dims_u[ax] - 1 - idx
7084 } else {
7085 idx
7086 };
7087 in_flat += in_idx * strides[ax];
7088 }
7089 std::ptr::copy_nonoverlapping(
7090 src_base.add(in_flat * eb),
7091 dst_base.add(o * eb),
7092 eb,
7093 );
7094 }
7095 })
7096 }
7097
7098 Thunk::Copy { src, dst, len } => {
7099 let len = len as usize;
7100 Arc::new(move |base: *mut u8| unsafe {
7101 if src == dst || len == 0 {
7102 return;
7103 }
7104 let src_ptr = base.add(src) as *const f32;
7105 let dst_ptr = base.add(dst) as *mut f32;
7106 if src_ptr == dst_ptr {
7107 return;
7108 }
7109 std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, len);
7110 })
7111 }
7112
7113 Thunk::Softmax { data, rows, cols } => {
7114 let (rows, cols) = (rows as usize, cols as usize);
7115 Arc::new(move |base: *mut u8| unsafe {
7116 crate::naive::softmax(sl_mut(data, base, rows * cols), rows, cols);
7117 })
7118 }
7119
7120 Thunk::Cumsum {
7121 src,
7122 dst,
7123 rows,
7124 cols,
7125 exclusive,
7126 } => {
7127 let (rows, cols) = (rows as usize, cols as usize);
7128 Arc::new(move |base: *mut u8| unsafe {
7129 let s = sl(src, base, rows * cols);
7130 let d = sl_mut(dst, base, rows * cols);
7131 if exclusive {
7132 for r in 0..rows {
7133 let mut acc = 0.0f32;
7134 for c in 0..cols {
7135 d[r * cols + c] = acc;
7136 acc += s[r * cols + c];
7137 }
7138 }
7139 } else {
7140 for r in 0..rows {
7141 let mut acc = 0.0f32;
7142 for c in 0..cols {
7143 acc += s[r * cols + c];
7144 d[r * cols + c] = acc;
7145 }
7146 }
7147 }
7148 })
7149 }
7150
7151 Thunk::Sample {
7152 logits,
7153 dst,
7154 batch,
7155 vocab,
7156 top_k,
7157 top_p,
7158 temperature,
7159 seed,
7160 } => {
7161 let (b, v) = (batch as usize, vocab as usize);
7162 let k = (top_k as usize).min(v);
7163 Arc::new(move |base: *mut u8| unsafe {
7164 let lg = sl(logits, base, b * v);
7165 let out = sl_mut(dst, base, b);
7166 let mut rng =
7167 rlx_ir::Philox4x32::new(if seed == 0 { 0xDEADBEEF } else { seed });
7168 for bi in 0..b {
7169 let row = &lg[bi * v..(bi + 1) * v];
7170 out[bi] = sample_row(row, k, top_p, temperature, &mut rng) as f32;
7171 }
7172 })
7173 }
7174
7175 Thunk::RngNormal {
7176 dst,
7177 len,
7178 mean,
7179 scale,
7180 key,
7181 op_seed,
7182 } => {
7183 let n = len as usize;
7184 let rng = rng_shared.clone();
7185 Arc::new(move |base: *mut u8| unsafe {
7186 let out = sl_mut(dst, base, n);
7187 let opts = *rng.read().unwrap();
7188 rlx_ir::fill_normal_like(out, mean, scale, opts, key, op_seed);
7189 })
7190 }
7191
7192 Thunk::RngUniform {
7193 dst,
7194 len,
7195 low,
7196 high,
7197 key,
7198 op_seed,
7199 } => {
7200 let n = len as usize;
7201 let rng = rng_shared.clone();
7202 Arc::new(move |base: *mut u8| unsafe {
7203 let out = sl_mut(dst, base, n);
7204 let opts = *rng.read().unwrap();
7205 rlx_ir::fill_uniform_like(out, low, high, opts, key, op_seed);
7206 })
7207 }
7208
7209 Thunk::DequantMatMul {
7210 x,
7211 w_q,
7212 scale,
7213 zp,
7214 dst,
7215 m,
7216 k,
7217 n,
7218 block_size,
7219 is_asymmetric,
7220 } => {
7221 let (m, k, n, bs) = (m as usize, k as usize, n as usize, block_size as usize);
7222 let n_blocks_per_col = k.div_ceil(bs);
7223 Arc::new(move |base: *mut u8| unsafe {
7224 let xs = sl(x, base, m * k);
7225 let raw = base.add(w_q);
7227 let w_bytes = std::slice::from_raw_parts(raw as *const i8, k * n);
7228 let scales = sl(scale, base, n_blocks_per_col * n);
7229 let zps = if is_asymmetric {
7230 sl(zp, base, n_blocks_per_col * n)
7231 } else {
7232 &[][..]
7233 };
7234 let out = sl_mut(dst, base, m * n);
7235 dequant_matmul_int8(
7236 xs,
7237 w_bytes,
7238 scales,
7239 zps,
7240 out,
7241 m,
7242 k,
7243 n,
7244 bs,
7245 is_asymmetric,
7246 );
7247 })
7248 }
7249
7250 Thunk::DequantMatMulGguf {
7251 x,
7252 w_q,
7253 dst,
7254 m,
7255 k,
7256 n,
7257 scheme,
7258 } => {
7259 let (m, k, n) = (m as usize, k as usize, n as usize);
7260 let block_bytes = scheme.gguf_block_bytes() as usize;
7261 let block_elems = scheme.gguf_block_size() as usize;
7262 let total_bytes = (k * n) / block_elems * block_bytes;
7263 Arc::new(move |base: *mut u8| unsafe {
7264 let xs = sl(x, base, m * k);
7265 let w_bytes =
7266 std::slice::from_raw_parts(base.add(w_q) as *const u8, total_bytes);
7267 let out = sl_mut(dst, base, m * n);
7268 crate::gguf_matmul::gguf_matmul_bt(xs, w_bytes, out, m, k, n, scheme);
7269 })
7270 }
7271
7272 Thunk::DequantMatMulInt4 {
7273 x,
7274 w_q,
7275 scale,
7276 zp,
7277 dst,
7278 m,
7279 k,
7280 n,
7281 block_size,
7282 is_asymmetric,
7283 } => {
7284 let (m, k, n, bs) = (m as usize, k as usize, n as usize, block_size as usize);
7285 let n_blocks = k.div_ceil(bs);
7286 Arc::new(move |base: *mut u8| unsafe {
7287 let xs = sl(x, base, m * k);
7288 let w_bytes = std::slice::from_raw_parts(
7289 base.add(w_q) as *const u8,
7290 (k * n).div_ceil(2),
7291 );
7292 let scales = sl(scale, base, n_blocks * n);
7293 let zps = if is_asymmetric {
7294 sl(zp, base, n_blocks * n)
7295 } else {
7296 &[][..]
7297 };
7298 let out = sl_mut(dst, base, m * n);
7299 dequant_matmul_int4(
7300 xs,
7301 w_bytes,
7302 scales,
7303 zps,
7304 out,
7305 m,
7306 k,
7307 n,
7308 bs,
7309 is_asymmetric,
7310 );
7311 })
7312 }
7313
7314 Thunk::DequantMatMulFp8 {
7315 x,
7316 w_q,
7317 scale,
7318 dst,
7319 m,
7320 k,
7321 n,
7322 e5m2,
7323 } => {
7324 let (m, k, n) = (m as usize, k as usize, n as usize);
7325 Arc::new(move |base: *mut u8| unsafe {
7326 let xs = sl(x, base, m * k);
7327 let w_bytes = std::slice::from_raw_parts(base.add(w_q) as *const u8, k * n);
7328 let scales = sl(scale, base, n);
7329 let out = sl_mut(dst, base, m * n);
7330 dequant_matmul_fp8(xs, w_bytes, scales, out, m, k, n, e5m2);
7331 })
7332 }
7333
7334 Thunk::DequantMatMulNvfp4 {
7335 x,
7336 w_q,
7337 scale,
7338 global_scale,
7339 dst,
7340 m,
7341 k,
7342 n,
7343 } => {
7344 let (m, k, n) = (m as usize, k as usize, n as usize);
7345 let n_scale = k.div_ceil(rlx_ir::NVFP4_GROUP_SIZE) * n;
7346 Arc::new(move |base: *mut u8| unsafe {
7347 let xs = sl(x, base, m * k);
7348 let w_bytes = std::slice::from_raw_parts(
7349 base.add(w_q) as *const u8,
7350 (k * n).div_ceil(2),
7351 );
7352 let scale_bytes =
7353 std::slice::from_raw_parts(base.add(scale) as *const u8, n_scale);
7354 let gs = sl(global_scale, base, 1)[0];
7355 let out = sl_mut(dst, base, m * n);
7356 dequant_matmul_nvfp4(xs, w_bytes, scale_bytes, gs, out, m, k, n);
7357 })
7358 }
7359
7360 Thunk::ScaledMatMul {
7361 lhs,
7362 rhs,
7363 lhs_scale,
7364 rhs_scale,
7365 bias,
7366 dst,
7367 m,
7368 k,
7369 n,
7370 lhs_fmt,
7371 rhs_fmt,
7372 layout,
7373 has_bias,
7374 } => {
7375 let (m, k, n) = (m as usize, k as usize, n as usize);
7376 let nblk = lowp_nblk(k, layout);
7377 let per_tensor = matches!(layout, rlx_ir::ScaleLayout::PerTensor);
7378 let n_lscale = if per_tensor { 1 } else { m * nblk };
7379 let n_rscale = if per_tensor { 1 } else { n * nblk };
7380 Arc::new(move |base: *mut u8| unsafe {
7381 let lhs_b = std::slice::from_raw_parts(base.add(lhs) as *const u8, m * k);
7382 let rhs_b = std::slice::from_raw_parts(base.add(rhs) as *const u8, n * k);
7383 let ls = lowp_read_scales(layout, base, lhs_scale, n_lscale);
7384 let rs = lowp_read_scales(layout, base, rhs_scale, n_rscale);
7385 let bias_s = if has_bias { Some(sl(bias, base, n)) } else { None };
7386 let out = sl_mut(dst, base, m * n);
7387 lowp_scaled_matmul(
7388 lhs_b, rhs_b, &ls, &rs, bias_s, out, m, n, k, layout, lhs_fmt, rhs_fmt,
7389 );
7390 })
7391 }
7392
7393 Thunk::ScaledQuantize {
7394 x,
7395 scale,
7396 dst,
7397 rows,
7398 cols,
7399 fmt,
7400 layout,
7401 } => {
7402 let (rows, cols) = (rows as usize, cols as usize);
7403 let nblk = lowp_nblk(cols, layout);
7404 let n_scale = if matches!(layout, rlx_ir::ScaleLayout::PerTensor) {
7405 1
7406 } else {
7407 rows * nblk
7408 };
7409 Arc::new(move |base: *mut u8| unsafe {
7410 let xs = sl(x, base, rows * cols);
7411 let scales = lowp_read_scales(layout, base, scale, n_scale);
7412 let out =
7413 std::slice::from_raw_parts_mut(base.add(dst), rows * cols);
7414 lowp_quantize(xs, &scales, fmt, layout, rows, cols, out);
7415 })
7416 }
7417
7418 Thunk::ScaledQuantScale {
7419 x,
7420 dst,
7421 rows,
7422 cols,
7423 fmt,
7424 layout,
7425 } => {
7426 let (rows, cols) = (rows as usize, cols as usize);
7427 let nblk = lowp_nblk(cols, layout);
7428 Arc::new(move |base: *mut u8| unsafe {
7429 let xs = sl(x, base, rows * cols);
7430 let scales = lowp_compute_scales(xs, fmt, layout, rows, cols);
7431 match layout {
7432 rlx_ir::ScaleLayout::PerTensor => {
7433 sl_mut(dst, base, 1)[0] = scales[0];
7434 }
7435 rlx_ir::ScaleLayout::BlockMxE8M0 { .. } => {
7436 let out = std::slice::from_raw_parts_mut(
7437 base.add(dst),
7438 rows * nblk,
7439 );
7440 for (o, &s) in out.iter_mut().zip(&scales) {
7441 *o = rlx_ir::lowp_codec::f32_to_e8m0(s);
7442 }
7443 }
7444 rlx_ir::ScaleLayout::Nvfp4 { .. } => {
7445 let out = std::slice::from_raw_parts_mut(
7446 base.add(dst),
7447 rows * nblk,
7448 );
7449 for (o, &s) in out.iter_mut().zip(&scales) {
7450 *o = rlx_ir::lowp_codec::encode(rlx_ir::ScaledFormat::F8E4M3, s);
7451 }
7452 }
7453 }
7454 })
7455 }
7456
7457 Thunk::ScaledDequantize {
7458 codes,
7459 scale,
7460 dst,
7461 rows,
7462 cols,
7463 fmt,
7464 layout,
7465 } => {
7466 let (rows, cols) = (rows as usize, cols as usize);
7467 let nblk = lowp_nblk(cols, layout);
7468 let n_scale = if matches!(layout, rlx_ir::ScaleLayout::PerTensor) {
7469 1
7470 } else {
7471 rows * nblk
7472 };
7473 Arc::new(move |base: *mut u8| unsafe {
7474 let cs = std::slice::from_raw_parts(base.add(codes) as *const u8, rows * cols);
7475 let scales = lowp_read_scales(layout, base, scale, n_scale);
7476 let out = sl_mut(dst, base, rows * cols);
7477 lowp_dequantize(cs, &scales, fmt, layout, rows, cols, out);
7478 })
7479 }
7480
7481 Thunk::LoraMatMul {
7482 x,
7483 w,
7484 a,
7485 b,
7486 dst,
7487 m,
7488 k,
7489 n,
7490 r,
7491 scale,
7492 } => {
7493 let (m, k, n, r) = (m as usize, k as usize, n as usize, r as usize);
7494 Arc::new(move |base: *mut u8| unsafe {
7495 let xs = sl(x, base, m * k);
7496 let ws = sl(w, base, k * n);
7497 let a_s = sl(a, base, k * r);
7498 let bs = sl(b, base, r * n);
7499 let out = sl_mut(dst, base, m * n);
7500 crate::blas::sgemm(xs, ws, out, m, k, n);
7502 let mut tmp = vec![0f32; m * r];
7504 crate::blas::sgemm(xs, a_s, &mut tmp, m, k, r);
7505 if scale != 1.0 {
7509 for v in tmp.iter_mut() {
7510 *v *= scale;
7511 }
7512 }
7513 crate::blas::sgemm_accumulate(&tmp, bs, out, m, r, n);
7514 })
7515 }
7516
7517 Thunk::LayerNorm {
7518 src,
7519 g,
7520 b,
7521 dst,
7522 rows,
7523 h,
7524 eps,
7525 } => {
7526 let (rows, h) = (rows as usize, h as usize);
7527 Arc::new(move |base: *mut u8| unsafe {
7528 let inp = sl(src, base, rows * h);
7529 let gamma = sl(g, base, h);
7530 let beta = sl(b, base, h);
7531 let out = sl_mut(dst, base, rows * h);
7532 for row in 0..rows {
7533 crate::kernels::layer_norm_row(
7534 &inp[row * h..(row + 1) * h],
7535 gamma,
7536 beta,
7537 &mut out[row * h..(row + 1) * h],
7538 h,
7539 eps,
7540 );
7541 }
7542 })
7543 }
7544
7545 Thunk::BatchNormInference {
7546 src,
7547 g,
7548 b,
7549 mean,
7550 var,
7551 dst,
7552 count,
7553 channels,
7554 eps,
7555 } => {
7556 let count = count as usize;
7557 let c = channels as usize;
7558 let n = count * c;
7559 let (src, g, b, mean, var, dst) = (src, g, b, mean, var, dst);
7560 Arc::new(move |base: *mut u8| unsafe {
7561 crate::kernels::batch_norm_inference(
7562 sl(src, base, n),
7563 sl(g, base, c),
7564 sl(b, base, c),
7565 sl(mean, base, c),
7566 sl(var, base, c),
7567 sl_mut(dst, base, n),
7568 c,
7569 eps,
7570 );
7571 })
7572 }
7573
7574 Thunk::Attention {
7575 q,
7576 k,
7577 v,
7578 mask,
7579 out,
7580 batch,
7581 seq,
7582 kv_seq,
7583 heads,
7584 head_dim,
7585 mask_kind,
7586 scale,
7587 q_row_stride,
7588 k_row_stride,
7589 v_row_stride,
7590 bhsd,
7591 } => {
7592 if std::env::var("RLX_ATTN_DEBUG").is_ok() {
7593 eprintln!("[attn-compile] batch={batch} seq={seq} kv_seq={kv_seq} heads={heads} bhsd={bhsd}");
7594 }
7595 let (b, q_s, k_s, nh, dh) = (
7604 batch as usize,
7605 seq as usize,
7606 kv_seq as usize,
7607 heads as usize,
7608 head_dim as usize,
7609 );
7610 let hs = nh * dh;
7611 let qrs = q_row_stride as usize;
7612 let krs = k_row_stride as usize;
7613 let vrs = v_row_stride as usize;
7614 Arc::new(move |base: *mut u8| unsafe {
7616 if std::env::var("RLX_ATTN_DEBUG").is_ok() {
7617 eprintln!("[attn] b={b} q_s={q_s} k_s={k_s} nh={nh} dh={dh} bhsd={bhsd} mask_kind={:?}", mask_kind);
7618 }
7619 let (q_len, k_len, v_len, o_len) = if bhsd {
7624 let qn = b * nh * q_s * dh;
7625 let kn = b * nh * k_s * dh;
7626 (qn, kn, kn, qn)
7627 } else {
7628 (b * q_s * qrs, b * k_s * krs, b * k_s * vrs, b * q_s * hs)
7629 };
7630 let q_d = sl(q, base, q_len);
7631 let k_d = sl(k, base, k_len);
7632 let v_d = sl(v, base, v_len);
7633 let m_d: &[f32] = match mask_kind {
7634 rlx_ir::op::MaskKind::Custom => sl(mask, base, b * k_s),
7635 rlx_ir::op::MaskKind::Bias => sl(mask, base, b * nh * q_s * k_s),
7636 _ => &[],
7637 };
7638 let o_d = sl_mut(out, base, o_len);
7639 let mut qh = vec![0f32; q_s * dh];
7640 let mut kh = vec![0f32; k_s * dh];
7641 let mut vh = vec![0f32; k_s * dh];
7642 let mut sc = vec![0f32; q_s * k_s];
7643 let mut oh = vec![0f32; q_s * dh];
7644 for bi in 0..b {
7645 for hi in 0..nh {
7646 for si in 0..q_s {
7648 let q_off = if bhsd {
7649 bi * nh * q_s * dh + hi * q_s * dh + si * dh
7650 } else {
7651 bi * q_s * qrs + si * qrs + hi * dh
7652 };
7653 qh[si * dh..(si + 1) * dh]
7654 .copy_from_slice(&q_d[q_off..q_off + dh]);
7655 }
7656 for si in 0..k_s {
7658 let (k_off, v_off) = if bhsd {
7659 (
7660 bi * nh * k_s * dh + hi * k_s * dh + si * dh,
7661 bi * nh * k_s * dh + hi * k_s * dh + si * dh,
7662 )
7663 } else {
7664 (
7665 bi * k_s * krs + si * krs + hi * dh,
7666 bi * k_s * vrs + si * vrs + hi * dh,
7667 )
7668 };
7669 kh[si * dh..(si + 1) * dh]
7670 .copy_from_slice(&k_d[k_off..k_off + dh]);
7671 vh[si * dh..(si + 1) * dh]
7672 .copy_from_slice(&v_d[v_off..v_off + dh]);
7673 }
7674 for qi in 0..q_s {
7675 for ki in 0..k_s {
7676 let mut dot = 0f32;
7677 for d in 0..dh {
7678 dot += qh[qi * dh + d] * kh[ki * dh + d];
7679 }
7680 sc[qi * k_s + ki] = dot * scale;
7681 }
7682 }
7683 let q_offset = k_s.saturating_sub(q_s);
7687 match mask_kind {
7688 rlx_ir::op::MaskKind::None => {}
7689 rlx_ir::op::MaskKind::Causal => {
7690 for qi in 0..q_s {
7691 let abs_q = q_offset + qi;
7692 for ki in (abs_q + 1)..k_s {
7693 sc[qi * k_s + ki] = mask_neg;
7694 }
7695 }
7696 }
7697 rlx_ir::op::MaskKind::SlidingWindow(w) => {
7698 for qi in 0..q_s {
7699 let abs_q = q_offset + qi;
7700 let lo = abs_q.saturating_sub(w);
7701 for ki in 0..k_s {
7702 if ki < lo || ki > abs_q {
7703 sc[qi * k_s + ki] = mask_neg;
7704 }
7705 }
7706 }
7707 }
7708 rlx_ir::op::MaskKind::Custom => {
7709 for qi in 0..q_s {
7710 for ki in 0..k_s {
7711 if m_d[bi * k_s + ki] < mask_thr {
7712 sc[qi * k_s + ki] = mask_neg;
7713 }
7714 }
7715 }
7716 }
7717 rlx_ir::op::MaskKind::Bias => {
7718 let per_bh = q_s * k_s;
7719 let off = (bi * nh + hi) * per_bh;
7720 for i in 0..per_bh {
7721 sc[i] += m_d[off + i];
7722 }
7723 }
7724 }
7725 crate::naive::softmax(&mut sc, q_s, k_s);
7726 oh.fill(0.0);
7727 for qi in 0..q_s {
7728 for ki in 0..k_s {
7729 let w = sc[qi * k_s + ki];
7730 if w > score_skip {
7731 for d in 0..dh {
7732 oh[qi * dh + d] += w * vh[ki * dh + d];
7733 }
7734 }
7735 }
7736 }
7737 for si in 0..q_s {
7738 let off = if bhsd {
7739 bi * nh * q_s * dh + hi * q_s * dh + si * dh
7740 } else {
7741 bi * q_s * hs + si * hs + hi * dh
7742 };
7743 o_d[off..off + dh].copy_from_slice(&oh[si * dh..(si + 1) * dh]);
7744 }
7745 }
7746 }
7747 })
7748 }
7749
7750 Thunk::FusedSwiGLU {
7751 src,
7752 dst,
7753 n_half,
7754 total,
7755 gate_first,
7756 } => {
7757 let n = n_half as usize;
7758 let t = total as usize;
7759 let outer = t / n;
7760 let in_total = outer * 2 * n;
7761 Arc::new(move |base: *mut u8| unsafe {
7762 let inp = sl(src, base, in_total);
7763 let out = sl_mut(dst, base, t);
7764 for o in 0..outer {
7765 let in_row = &inp[o * 2 * n..(o + 1) * 2 * n];
7766 let out_row = &mut out[o * n..(o + 1) * n];
7767 for i in 0..n {
7768 let (up, gate) = if gate_first {
7769 (in_row[n + i], in_row[i])
7770 } else {
7771 (in_row[i], in_row[n + i])
7772 };
7773 out_row[i] = up * (gate / (1.0 + (-gate).exp()));
7774 }
7775 }
7776 })
7777 }
7778
7779 Thunk::Concat {
7780 dst,
7781 outer,
7782 inner,
7783 total_axis,
7784 inputs,
7785 } => {
7786 let outer = outer as usize;
7787 let inner = inner as usize;
7788 let total_axis = total_axis as usize;
7789 let out_total = outer * total_axis * inner;
7790 let mut layout: Vec<(usize, usize, usize, usize)> =
7791 Vec::with_capacity(inputs.len());
7792 let mut cum: usize = 0;
7793 for (src_off, in_axis, in_numel) in &inputs {
7794 let in_axis = *in_axis as usize;
7795 layout.push((*src_off, cum * inner, in_axis * inner, *in_numel as usize));
7796 cum += in_axis;
7797 }
7798 Arc::new(move |base: *mut u8| unsafe {
7799 let out = sl_mut(dst, base, out_total);
7800 let row_stride = total_axis * inner;
7801 for (src_off, dst_col_off, copy_per_row, in_numel) in &layout {
7802 let inp = sl(*src_off, base, (*in_numel).max(1));
7803 concat_copy_rows_f32(
7804 out,
7805 inp,
7806 outer,
7807 *copy_per_row,
7808 row_stride,
7809 *dst_col_off,
7810 *in_numel,
7811 );
7812 }
7813 })
7814 }
7815
7816 Thunk::CustomOp {
7817 kernel,
7818 inputs,
7819 output,
7820 attrs,
7821 } => {
7822 let kernel = kernel.clone();
7828 let attrs = attrs.clone();
7829 let inputs = inputs.clone();
7830 let (out_off, out_len, out_shape) = output.clone();
7831 Arc::new(move |base: *mut u8| unsafe {
7832 dispatch_custom_op(
7833 &*kernel, &inputs, out_off, out_len, &out_shape, &attrs, base,
7834 );
7835 })
7836 }
7837
7838 Thunk::GaussianSplatRender {
7839 positions_off,
7840 positions_len,
7841 scales_off,
7842 scales_len,
7843 rotations_off,
7844 rotations_len,
7845 opacities_off,
7846 opacities_len,
7847 colors_off,
7848 colors_len,
7849 sh_coeffs_off,
7850 sh_coeffs_len,
7851 meta_off,
7852 dst_off,
7853 dst_len,
7854 width,
7855 height,
7856 tile_size,
7857 radius_scale,
7858 alpha_cutoff,
7859 max_splat_steps,
7860 transmittance_threshold,
7861 max_list_entries,
7862 } => Arc::new(move |base: *mut u8| unsafe {
7863 crate::splat::execute_gaussian_splat_render(
7864 positions_off,
7865 positions_len,
7866 scales_off,
7867 scales_len,
7868 rotations_off,
7869 rotations_len,
7870 opacities_off,
7871 opacities_len,
7872 colors_off,
7873 colors_len,
7874 sh_coeffs_off,
7875 sh_coeffs_len,
7876 meta_off,
7877 dst_off,
7878 dst_len,
7879 width,
7880 height,
7881 tile_size,
7882 radius_scale,
7883 alpha_cutoff,
7884 max_splat_steps,
7885 transmittance_threshold,
7886 max_list_entries,
7887 base,
7888 );
7889 }),
7890
7891 Thunk::GaussianSplatRenderBackward {
7892 positions_off,
7893 positions_len,
7894 scales_off,
7895 scales_len,
7896 rotations_off,
7897 rotations_len,
7898 opacities_off,
7899 opacities_len,
7900 colors_off,
7901 colors_len,
7902 sh_coeffs_off,
7903 sh_coeffs_len,
7904 meta_off,
7905 d_loss_off,
7906 d_loss_len,
7907 packed_off,
7908 packed_len,
7909 width,
7910 height,
7911 tile_size,
7912 radius_scale,
7913 alpha_cutoff,
7914 max_splat_steps,
7915 transmittance_threshold,
7916 max_list_entries,
7917 loss_grad_clip,
7918 sh_band,
7919 max_anisotropy,
7920 } => Arc::new(move |base: *mut u8| unsafe {
7921 crate::splat::execute_gaussian_splat_render_backward(
7922 positions_off,
7923 positions_len,
7924 scales_off,
7925 scales_len,
7926 rotations_off,
7927 rotations_len,
7928 opacities_off,
7929 opacities_len,
7930 colors_off,
7931 colors_len,
7932 sh_coeffs_off,
7933 sh_coeffs_len,
7934 meta_off,
7935 d_loss_off,
7936 d_loss_len,
7937 packed_off,
7938 packed_len,
7939 width,
7940 height,
7941 tile_size,
7942 radius_scale,
7943 alpha_cutoff,
7944 max_splat_steps,
7945 transmittance_threshold,
7946 max_list_entries,
7947 loss_grad_clip,
7948 sh_band,
7949 max_anisotropy,
7950 base,
7951 );
7952 }),
7953
7954 Thunk::GaussianSplatPrepare {
7955 positions_off,
7956 positions_len,
7957 scales_off,
7958 scales_len,
7959 rotations_off,
7960 rotations_len,
7961 opacities_off,
7962 opacities_len,
7963 colors_off,
7964 colors_len,
7965 sh_coeffs_off,
7966 sh_coeffs_len,
7967 meta_off,
7968 meta_len,
7969 prep_off,
7970 prep_len,
7971 width,
7972 height,
7973 tile_size,
7974 radius_scale,
7975 alpha_cutoff,
7976 max_splat_steps,
7977 transmittance_threshold,
7978 max_list_entries,
7979 } => Arc::new(move |base: *mut u8| unsafe {
7980 crate::splat::execute_gaussian_splat_prepare(
7981 positions_off,
7982 positions_len,
7983 scales_off,
7984 scales_len,
7985 rotations_off,
7986 rotations_len,
7987 opacities_off,
7988 opacities_len,
7989 colors_off,
7990 colors_len,
7991 sh_coeffs_off,
7992 sh_coeffs_len,
7993 meta_off,
7994 meta_len,
7995 prep_off,
7996 prep_len,
7997 width,
7998 height,
7999 tile_size,
8000 radius_scale,
8001 alpha_cutoff,
8002 max_splat_steps,
8003 transmittance_threshold,
8004 max_list_entries,
8005 base,
8006 );
8007 }),
8008
8009 Thunk::GaussianSplatRasterize {
8010 prep_off,
8011 prep_len,
8012 meta_off,
8013 meta_len,
8014 dst_off,
8015 dst_len,
8016 count,
8017 width,
8018 height,
8019 tile_size,
8020 alpha_cutoff,
8021 max_splat_steps,
8022 transmittance_threshold,
8023 max_list_entries,
8024 } => Arc::new(move |base: *mut u8| unsafe {
8025 crate::splat::execute_gaussian_splat_rasterize(
8026 prep_off,
8027 prep_len,
8028 meta_off,
8029 meta_len,
8030 dst_off,
8031 dst_len,
8032 count,
8033 width,
8034 height,
8035 tile_size,
8036 alpha_cutoff,
8037 max_splat_steps,
8038 transmittance_threshold,
8039 max_list_entries,
8040 base,
8041 );
8042 }),
8043
8044 Thunk::Fft1d {
8045 src,
8046 dst,
8047 outer,
8048 n_complex,
8049 inverse,
8050 norm_tag,
8051 dtype,
8052 } => {
8053 let f: Arc<dyn Fn(*mut u8) + Send + Sync> = match dtype {
8054 rlx_ir::DType::F64 => Arc::new(move |base: *mut u8| unsafe {
8055 execute_fft1d_f64(
8056 src,
8057 dst,
8058 outer as usize,
8059 n_complex as usize,
8060 inverse,
8061 norm_tag,
8062 base,
8063 );
8064 }),
8065 rlx_ir::DType::F32 => Arc::new(move |base: *mut u8| unsafe {
8066 execute_fft1d_f32(
8067 src,
8068 dst,
8069 outer as usize,
8070 n_complex as usize,
8071 inverse,
8072 norm_tag,
8073 base,
8074 );
8075 }),
8076 rlx_ir::DType::C64 => Arc::new(move |base: *mut u8| unsafe {
8077 execute_fft1d_c64(
8078 src,
8079 dst,
8080 outer as usize,
8081 n_complex as usize,
8082 inverse,
8083 norm_tag,
8084 base,
8085 );
8086 }),
8087 other => panic!("Op::Fft on CPU requires F32/F64/C64, got {other:?}"),
8088 };
8089 f
8090 }
8091
8092 Thunk::FftButterflyStage {
8093 state_src,
8094 state_dst,
8095 gate_src,
8096 rev_src,
8097 tw_re_src,
8098 tw_im_src,
8099 batch,
8100 n_fft,
8101 stage,
8102 } => Arc::new(move |base: *mut u8| unsafe {
8103 execute_fft_butterfly_stage_f32(
8104 state_src,
8105 state_dst,
8106 gate_src,
8107 rev_src,
8108 tw_re_src,
8109 tw_im_src,
8110 batch as usize,
8111 n_fft as usize,
8112 stage as usize,
8113 base,
8114 );
8115 }),
8116
8117 Thunk::LogMel {
8118 spec,
8119 filters,
8120 dst,
8121 outer,
8122 n_fft,
8123 n_bins,
8124 n_mels,
8125 } => Arc::new(move |base: *mut u8| unsafe {
8126 execute_log_mel_f32(
8127 spec,
8128 filters,
8129 dst,
8130 outer as usize,
8131 n_fft as usize,
8132 n_bins as usize,
8133 n_mels as usize,
8134 base,
8135 );
8136 }),
8137
8138 Thunk::LogMelBackward {
8139 spec,
8140 filters,
8141 dy,
8142 dst,
8143 outer,
8144 n_fft,
8145 n_bins,
8146 n_mels,
8147 } => Arc::new(move |base: *mut u8| unsafe {
8148 execute_log_mel_backward_f32(
8149 spec,
8150 filters,
8151 dy,
8152 dst,
8153 outer as usize,
8154 n_fft as usize,
8155 n_bins as usize,
8156 n_mels as usize,
8157 base,
8158 );
8159 }),
8160
8161 Thunk::WelchPeaks {
8162 spec,
8163 dst,
8164 welch_batch,
8165 n_fft,
8166 n_segments,
8167 k,
8168 } => Arc::new(move |base: *mut u8| unsafe {
8169 execute_welch_peaks_f32(
8170 spec,
8171 dst,
8172 welch_batch as usize,
8173 n_fft as usize,
8174 n_segments as usize,
8175 k as usize,
8176 base,
8177 );
8178 }),
8179
8180 Thunk::SgdMomentum { param, vel, grad, p_out, v_out, lr, mom, len } => {
8181 let len = len as usize;
8182 Arc::new(move |base: *mut u8| unsafe {
8183 let p = sl(param, base, len);
8184 let v = sl(vel, base, len);
8185 let g = sl(grad, base, len);
8186 let po = sl_mut(p_out, base, len);
8187 let vo = sl_mut(v_out, base, len);
8188 for i in 0..len {
8189 let vn = mom * v[i] + g[i];
8190 vo[i] = vn;
8191 po[i] = p[i] - lr * vn;
8192 }
8193 })
8194 }
8195
8196 _ => Arc::new(|_: *mut u8| {}),
8197 }
8198 })
8199 .collect();
8200
8201 let fuse_threshold: usize = rlx_ir::env::var("RLX_FUSE_ATTN_THRESHOLD")
8205 .and_then(|v| v.parse().ok())
8206 .unwrap_or(64);
8207 let should_fuse = thunks.iter().any(|t| match t {
8208 Thunk::Attention { batch, seq, .. } => {
8209 (*batch as usize) * (*seq as usize) <= fuse_threshold
8210 }
8211 _ => false,
8212 });
8213
8214 if should_fuse {
8215 let active: Vec<usize> = thunks
8217 .iter()
8218 .enumerate()
8219 .filter(|(_, t)| !matches!(t, Thunk::Nop))
8220 .map(|(i, _)| i)
8221 .collect();
8222
8223 let mut kill = vec![false; thunks.len()]; let mut insertions: Vec<(usize, Thunk)> = Vec::new(); let mut ai = 0;
8227 while ai < active.len() {
8228 let a = |off: usize| -> Option<(usize, &Thunk)> {
8230 active.get(ai + off).map(|&idx| (idx, &thunks[idx]))
8231 };
8232
8233 let matched = (|| {
8235 let (_i0, t0) = a(0)?;
8236 let (_, t1) = a(1)?;
8237 let (_, t2) = a(2)?;
8238 let (_, t3) = a(3)?;
8239
8240 let (hidden, qkv_w, qkv_b, has_b) = match t0 {
8242 Thunk::FusedMmBiasAct {
8243 a,
8244 w,
8245 bias,
8246 n: _,
8247 act: None,
8248 ..
8249 } => (*a, *w, *bias, true),
8250 Thunk::Sgemm { a, b, n: _, .. } => (*a, *b, 0, false),
8251 _ => return None,
8252 };
8253
8254 if !matches!(t1, Thunk::Narrow { .. }) {
8256 return None;
8257 }
8258 if !matches!(t2, Thunk::Narrow { .. }) {
8259 return None;
8260 }
8261 if !matches!(t3, Thunk::Narrow { .. }) {
8262 return None;
8263 }
8264
8265 let (has_rope, attn_ai, cos_off, sin_off, cl, rope_interleaved) = if let Some((
8270 _,
8271 Thunk::Rope {
8272 cos,
8273 sin,
8274 cos_len,
8275 interleaved,
8276 ..
8277 },
8278 )) = a(4)
8279 {
8280 let q_il = *interleaved;
8281 match a(5).map(|x| x.1) {
8282 Some(Thunk::Rope {
8283 interleaved: k_il, ..
8284 }) if *k_il == q_il => {
8285 if matches!(a(6).map(|x| x.1), Some(Thunk::Attention { .. })) {
8286 (true, 6, *cos, *sin, *cos_len, q_il)
8287 } else {
8288 return None;
8289 }
8290 }
8291 _ => return None,
8292 }
8293 } else if matches!(a(4).map(|x| x.1), Some(Thunk::Attention { .. })) {
8294 (false, 4, 0, 0, 0, false)
8295 } else {
8296 return None;
8297 };
8298
8299 let (_attn_real_idx, attn_t) = a(attn_ai)?;
8300 let (batch, seq, heads, head_dim, mask, mask_kind, kv_seq) = match attn_t {
8301 Thunk::Attention {
8302 batch,
8303 seq,
8304 heads,
8305 head_dim,
8306 mask,
8307 mask_kind,
8308 kv_seq,
8309 ..
8310 } => (*batch, *seq, *heads, *head_dim, *mask, *mask_kind, *kv_seq),
8311 _ => return None,
8312 };
8313 if matches!(mask_kind, rlx_ir::op::MaskKind::Bias) || kv_seq != seq {
8319 return None;
8320 }
8321
8322 let (_out_real_idx, out_t) = a(attn_ai + 1)?;
8324 let (out_w, out_b, out_dst) = match out_t {
8325 Thunk::FusedMmBiasAct {
8326 w,
8327 bias,
8328 c,
8329 act: None,
8330 ..
8331 } => (*w, *bias, *c),
8332 Thunk::Sgemm { b: w, c, .. } => (*w, 0, *c),
8333 _ => return None,
8334 };
8335
8336 let hs = heads * head_dim;
8337 let total_active = attn_ai + 2; Some((
8340 total_active,
8341 Thunk::FusedAttnBlock {
8342 hidden,
8343 qkv_w,
8344 out_w,
8345 mask,
8346 mask_kind,
8347 out: out_dst,
8348 qkv_b: if has_b { qkv_b } else { 0 },
8349 out_b: if has_b { out_b } else { 0 },
8350 cos: cos_off,
8351 sin: sin_off,
8352 cos_len: cl,
8353 batch,
8354 seq,
8355 hs,
8356 nh: heads,
8357 dh: head_dim,
8358 has_bias: has_b,
8359 has_rope,
8360 interleaved: rope_interleaved,
8361 },
8362 ))
8363 })();
8364
8365 if let Some((count, fused_thunk)) = matched {
8366 for off in 0..count {
8368 if let Some(&idx) = active.get(ai + off) {
8369 kill[idx] = true;
8370 }
8371 }
8372 insertions.push((active[ai], fused_thunk));
8374 ai += count;
8375 } else {
8376 ai += 1;
8377 }
8378 }
8379
8380 if !insertions.is_empty() {
8382 let mut new_thunks = Vec::with_capacity(thunks.len());
8383 let mut insert_idx = 0;
8384 for (i, t) in thunks.into_iter().enumerate() {
8385 if insert_idx < insertions.len() && insertions[insert_idx].0 == i {
8386 new_thunks.push(insertions[insert_idx].1.clone());
8387 insert_idx += 1;
8388 }
8389 if !kill[i] {
8390 new_thunks.push(t);
8391 }
8392 }
8393 if cfg.verbose >= 1 {
8394 eprintln!(
8395 "[rlx] fused_attention: {} attention blocks fused",
8396 insertions.len()
8397 );
8398 }
8399 thunks = new_thunks;
8400 }
8401 }
8402
8403 if should_fuse {
8408 let active: Vec<usize> = thunks
8409 .iter()
8410 .enumerate()
8411 .filter(|(_, t)| !matches!(t, Thunk::Nop))
8412 .map(|(i, _)| i)
8413 .collect();
8414
8415 let mut kill = vec![false; thunks.len()];
8416 let mut insertions: Vec<(usize, Thunk)> = Vec::new();
8417
8418 let a = |ai: usize| -> Option<&Thunk> { active.get(ai).map(|&i| &thunks[i]) };
8419
8420 let mut ai = 0;
8421 while ai < active.len() {
8422 let bert_match = (|| -> Option<usize> {
8424 let fab = a(ai)?;
8425 let rln1 = a(ai + 1)?;
8426 let ffn1 = a(ai + 2)?;
8427 let ffn2 = a(ai + 3)?;
8428 let rln2 = a(ai + 4)?;
8429
8430 let (hidden, qkv_w, qkv_b, out_w, out_b, mask, batch, seq, hs, nh, dh) = match fab {
8431 Thunk::FusedAttnBlock {
8432 hidden,
8433 qkv_w,
8434 qkv_b,
8435 out_w,
8436 out_b,
8437 mask,
8438 mask_kind: rlx_ir::op::MaskKind::Custom,
8442 batch,
8443 seq,
8444 hs,
8445 nh,
8446 dh,
8447 has_bias: true,
8448 has_rope: false,
8449 ..
8450 } => (
8451 *hidden, *qkv_w, *qkv_b, *out_w, *out_b, *mask, *batch, *seq, *hs, *nh, *dh,
8452 ),
8453 _ => return None,
8454 };
8455 let (ln1_g, ln1_b, eps1) = match rln1 {
8456 Thunk::FusedResidualLN { g, b, eps, .. } => (*g, *b, *eps),
8457 _ => return None,
8458 };
8459 let (fc1_w, fc1_b, int_dim) = match ffn1 {
8460 Thunk::FusedMmBiasAct {
8461 w,
8462 bias,
8463 n,
8464 act: Some(Activation::Gelu),
8465 ..
8466 } => (*w, *bias, *n),
8467 _ => return None,
8468 };
8469 let (fc2_w, fc2_b) = match ffn2 {
8470 Thunk::FusedMmBiasAct {
8471 w, bias, act: None, ..
8472 } => (*w, *bias),
8473 _ => return None,
8474 };
8475 let (ln2_g, ln2_b, eps2, out) = match rln2 {
8476 Thunk::FusedResidualLN { g, b, eps, out, .. } => (*g, *b, *eps, *out),
8477 _ => return None,
8478 };
8479
8480 for off in 0..5 {
8481 kill[active[ai + off]] = true;
8482 }
8483 insertions.push((
8484 active[ai],
8485 Thunk::FusedBertLayer {
8486 hidden,
8487 qkv_w,
8488 qkv_b,
8489 out_w,
8490 out_b,
8491 mask,
8492 ln1_g,
8493 ln1_b,
8494 eps1,
8495 fc1_w,
8496 fc1_b,
8497 fc2_w,
8498 fc2_b,
8499 ln2_g,
8500 ln2_b,
8501 eps2,
8502 out,
8503 batch,
8504 seq,
8505 hs,
8506 nh,
8507 dh,
8508 int_dim,
8509 },
8510 ));
8511 Some(5)
8512 })();
8513 if let Some(n) = bert_match {
8514 ai += n;
8515 continue;
8516 }
8517
8518 let nomic_match = (|| -> Option<usize> {
8535 if rlx_ir::env::flag("RLX_DISABLE_NOMIC_FUSION") {
8536 return None;
8537 }
8538 let (
8539 hidden,
8540 qkv_w,
8541 out_w,
8542 mask,
8543 cos,
8544 sin,
8545 cos_len,
8546 batch,
8547 seq,
8548 hs,
8549 nh,
8550 dh,
8551 interleaved,
8552 ) = match a(ai)? {
8553 Thunk::FusedAttnBlock {
8554 hidden,
8555 qkv_w,
8556 out_w,
8557 mask,
8558 cos,
8559 sin,
8560 cos_len,
8561 batch,
8562 seq,
8563 hs,
8564 nh,
8565 dh,
8566 has_bias: false,
8567 has_rope: true,
8568 mask_kind: rlx_ir::op::MaskKind::Custom,
8569 interleaved,
8570 ..
8571 } => (
8572 *hidden,
8573 *qkv_w,
8574 *out_w,
8575 *mask,
8576 *cos,
8577 *sin,
8578 *cos_len,
8579 *batch,
8580 *seq,
8581 *hs,
8582 *nh,
8583 *dh,
8584 *interleaved,
8585 ),
8586 _ => return None,
8587 };
8588 let (ln1_g, ln1_b, eps1) = match a(ai + 1)? {
8590 Thunk::FusedResidualLN { g, b, eps, .. } => (*g, *b, *eps),
8591 _ => return None,
8592 };
8593 let mut kills: Vec<usize> = vec![ai, ai + 1];
8596 let mut o = 2;
8598 if matches!(a(ai + o)?, Thunk::Concat { .. }) {
8599 o += 1;
8600 }
8601 let fused_fc_w = match a(ai + o)? {
8602 Thunk::Sgemm { b: w, .. } => *w,
8603 _ => return None,
8604 };
8605 kills.push(ai + o);
8606 o += 1;
8607 let int_dim = match a(ai + o)? {
8610 Thunk::FusedSwiGLU {
8611 n_half,
8612 gate_first: false,
8613 ..
8614 } => *n_half,
8615 _ => return None,
8616 };
8617 kills.push(ai + o);
8618 o += 1;
8619 let fc2_w = match a(ai + o)? {
8621 Thunk::Sgemm { b: w, .. } => *w,
8622 _ => return None,
8623 };
8624 kills.push(ai + o);
8625 o += 1;
8626 let (ln2_g, ln2_b, eps2, out) = match a(ai + o)? {
8628 Thunk::FusedResidualLN { g, b, eps, out, .. } => (*g, *b, *eps, *out),
8629 _ => return None,
8630 };
8631 kills.push(ai + o);
8632 let consumed = o + 1;
8633
8634 for ki in kills {
8635 kill[active[ki]] = true;
8636 }
8637 FUSED_NOMIC_LAYER_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8638 insertions.push((
8643 active[ai + o],
8644 Thunk::FusedNomicLayer {
8645 hidden,
8646 qkv_w,
8647 out_w,
8648 mask,
8649 cos,
8650 sin,
8651 cos_len,
8652 ln1_g,
8653 ln1_b,
8654 eps1,
8655 fc11_w: fused_fc_w,
8656 fc12_w: 0,
8657 fc2_w,
8658 ln2_g,
8659 ln2_b,
8660 eps2,
8661 out,
8662 batch,
8663 seq,
8664 hs,
8665 nh,
8666 dh,
8667 int_dim,
8668 interleaved,
8669 },
8670 ));
8671 Some(consumed)
8672 })();
8673 if let Some(n) = nomic_match {
8674 ai += n;
8675 continue;
8676 }
8677
8678 ai += 1;
8679 }
8680
8681 if !insertions.is_empty() {
8682 let mut new_thunks = Vec::with_capacity(thunks.len());
8683 let mut ins_idx = 0;
8684 for (i, t) in thunks.into_iter().enumerate() {
8685 if ins_idx < insertions.len() && insertions[ins_idx].0 == i {
8686 new_thunks.push(insertions[ins_idx].1.clone());
8687 ins_idx += 1;
8688 }
8689 if !kill[i] {
8690 new_thunks.push(t);
8691 }
8692 }
8693 if cfg.verbose >= 1 {
8694 eprintln!(
8695 "[rlx] fused_layer: {} full transformer layers fused",
8696 insertions.len()
8697 );
8698 }
8699 thunks = new_thunks;
8700 }
8701 }
8702
8703 {
8715 let mut read_offsets: HashMap<usize, usize> = HashMap::new();
8718 for t in &thunks {
8719 for off in thunk_read_offsets(t) {
8720 *read_offsets.entry(off).or_insert(0) += 1;
8721 }
8722 }
8723
8724 let mut fused_count = 0usize;
8725 for i in 0..thunks.len().saturating_sub(1) {
8726 let narrow = match &thunks[i] {
8729 Thunk::Narrow { .. } => i,
8730 _ => continue,
8731 };
8732 let mut j = narrow + 1;
8734 while j < thunks.len() && matches!(thunks[j], Thunk::Nop) {
8735 j += 1;
8736 }
8737 if j >= thunks.len() {
8738 continue;
8739 }
8740 let (n_src, n_dst, n_src_stride) = match &thunks[narrow] {
8742 Thunk::Narrow {
8743 src,
8744 dst,
8745 src_stride,
8746 ..
8747 } => (*src, *dst, *src_stride),
8748 _ => continue,
8749 };
8750 let rope_reads_narrow = matches!(&thunks[j],
8751 Thunk::Rope { src, .. } if *src == n_dst);
8752 if !rope_reads_narrow {
8753 continue;
8754 }
8755 if read_offsets.get(&n_dst).copied().unwrap_or(0) != 1 {
8759 continue;
8760 }
8761
8762 if let Thunk::Rope {
8765 src,
8766 src_row_stride,
8767 ..
8768 } = &mut thunks[j]
8769 {
8770 *src = n_src;
8771 *src_row_stride = n_src_stride;
8772 }
8773 thunks[narrow] = Thunk::Nop;
8774 fused_count += 1;
8775 }
8776
8777 if fused_count > 0 && cfg.verbose >= 1 {
8778 eprintln!(
8779 "[rlx] fused_qk_rope: {} Narrow→Rope pairs collapsed",
8780 fused_count
8781 );
8782 }
8783 }
8784
8785 {
8797 let mut read_counts: HashMap<usize, usize> = HashMap::new();
8798 for t in &thunks {
8799 for off in thunk_read_offsets(t) {
8800 *read_counts.entry(off).or_insert(0) += 1;
8801 }
8802 }
8803 let mut dst_to_idx: HashMap<usize, usize> = HashMap::new();
8805 for (i, t) in thunks.iter().enumerate() {
8806 if let Thunk::Narrow { dst, .. } = t {
8807 dst_to_idx.insert(*dst, i);
8808 }
8809 }
8810
8811 let mut fused_count = 0usize;
8812 for i in 0..thunks.len() {
8813 let (q_off, k_off, v_off) = match &thunks[i] {
8814 Thunk::Attention { q, k, v, .. } => (*q, *k, *v),
8815 _ => continue,
8816 };
8817 let q_n = match dst_to_idx.get(&q_off).copied() {
8819 Some(x) => x,
8820 None => continue,
8821 };
8822 let k_n = match dst_to_idx.get(&k_off).copied() {
8823 Some(x) => x,
8824 None => continue,
8825 };
8826 let v_n = match dst_to_idx.get(&v_off).copied() {
8827 Some(x) => x,
8828 None => continue,
8829 };
8830 if read_counts.get(&q_off).copied().unwrap_or(0) != 1 {
8832 continue;
8833 }
8834 if read_counts.get(&k_off).copied().unwrap_or(0) != 1 {
8835 continue;
8836 }
8837 if read_counts.get(&v_off).copied().unwrap_or(0) != 1 {
8838 continue;
8839 }
8840
8841 let (q_src, q_stride) = match &thunks[q_n] {
8842 Thunk::Narrow {
8843 src, src_stride, ..
8844 } => (*src, *src_stride),
8845 _ => continue,
8846 };
8847 let (k_src, k_stride) = match &thunks[k_n] {
8848 Thunk::Narrow {
8849 src, src_stride, ..
8850 } => (*src, *src_stride),
8851 _ => continue,
8852 };
8853 let (v_src, v_stride) = match &thunks[v_n] {
8854 Thunk::Narrow {
8855 src, src_stride, ..
8856 } => (*src, *src_stride),
8857 _ => continue,
8858 };
8859
8860 if let Thunk::Attention {
8861 q,
8862 k,
8863 v,
8864 q_row_stride,
8865 k_row_stride,
8866 v_row_stride,
8867 ..
8868 } = &mut thunks[i]
8869 {
8870 *q = q_src;
8871 *k = k_src;
8872 *v = v_src;
8873 *q_row_stride = q_stride;
8874 *k_row_stride = k_stride;
8875 *v_row_stride = v_stride;
8876 }
8877 thunks[q_n] = Thunk::Nop;
8878 thunks[k_n] = Thunk::Nop;
8879 thunks[v_n] = Thunk::Nop;
8880 fused_count += 1;
8881 }
8882
8883 if fused_count > 0 && cfg.verbose >= 1 {
8884 eprintln!(
8885 "[rlx] fused_strided_attn: {} Narrow×3→Attention rewrites",
8886 fused_count
8887 );
8888 }
8889 }
8890
8891 ThunkSchedule {
8892 thunks,
8893 moe_resident: None,
8894 moe_resident_layers: None,
8895 moe_topk_capture: None,
8896 mask_threshold: cfg.mask_binary_threshold,
8897 mask_neg_inf: cfg.attn_mask_neg_inf,
8898 score_skip: cfg.score_skip_threshold,
8899 compiled_fns,
8900 rng: rng_shared,
8901 }
8902}
8903
8904fn get_len(graph: &Graph, id: NodeId) -> usize {
8905 graph.node(id).shape.num_elements().unwrap_or(0)
8906}
8907
8908fn get_static_dims(graph: &Graph, id: NodeId) -> Vec<usize> {
8910 let dims = graph.node(id).shape.dims();
8911 let mut out = Vec::with_capacity(dims.len());
8912 for d in dims {
8913 if let Some(s) = match d {
8914 rlx_ir::Dim::Static(s) => Some(*s),
8915 _ => None,
8916 } {
8917 out.push(s);
8918 } else {
8919 return Vec::new();
8920 }
8921 }
8922 out
8923}
8924
8925fn concat_axis_extent(input: &rlx_ir::Shape, axis: usize, out_rank: usize) -> usize {
8928 let in_rank = input.rank();
8929 if axis >= out_rank {
8930 return 1;
8931 }
8932 if axis < in_rank {
8933 input.dim(axis).unwrap_static()
8934 } else {
8935 1
8936 }
8937}
8938
8939fn broadcast_src_index(src_idx: usize, in_len: usize) -> usize {
8940 if in_len == 0 { 0 } else { src_idx % in_len }
8941}
8942
8943fn concat_copy_rows_f32(
8944 out: &mut [f32],
8945 inp: &[f32],
8946 outer: usize,
8947 copy_per_row: usize,
8948 row_stride: usize,
8949 dst_col_off: usize,
8950 in_numel: usize,
8951) {
8952 let need = outer.saturating_mul(copy_per_row.max(1));
8953 let broadcast_outer = in_numel < need;
8954 for o in 0..outer {
8955 let dst_row_start = o * row_stride + dst_col_off;
8956 if broadcast_outer {
8957 if in_numel == 1 {
8958 if copy_per_row == 1 {
8959 out[dst_row_start] = inp[0];
8960 } else {
8961 out[dst_row_start..dst_row_start + copy_per_row].fill(inp[0]);
8962 }
8963 } else if copy_per_row <= inp.len() {
8964 out[dst_row_start..dst_row_start + copy_per_row]
8965 .copy_from_slice(&inp[..copy_per_row]);
8966 } else if !inp.is_empty() {
8967 out[dst_row_start..dst_row_start + copy_per_row].fill(inp[0]);
8968 }
8969 } else {
8970 let src_row_start = o * copy_per_row;
8971 out[dst_row_start..dst_row_start + copy_per_row]
8972 .copy_from_slice(&inp[src_row_start..src_row_start + copy_per_row]);
8973 }
8974 }
8975}
8976
8977fn concat_copy_rows_f64(
8978 out: &mut [f64],
8979 inp: &[f64],
8980 outer: usize,
8981 copy_per_row: usize,
8982 row_stride: usize,
8983 dst_col_off: usize,
8984 in_numel: usize,
8985) {
8986 let need = outer.saturating_mul(copy_per_row.max(1));
8987 let broadcast_outer = in_numel < need;
8988 for o in 0..outer {
8989 let dst_row_start = o * row_stride + dst_col_off;
8990 if broadcast_outer {
8991 if in_numel == 1 {
8992 if copy_per_row == 1 {
8993 out[dst_row_start] = inp[0];
8994 } else {
8995 out[dst_row_start..dst_row_start + copy_per_row].fill(inp[0]);
8996 }
8997 } else if copy_per_row <= inp.len() {
8998 out[dst_row_start..dst_row_start + copy_per_row]
8999 .copy_from_slice(&inp[..copy_per_row]);
9000 } else if !inp.is_empty() {
9001 out[dst_row_start..dst_row_start + copy_per_row].fill(inp[0]);
9002 }
9003 } else {
9004 let src_row_start = o * copy_per_row;
9005 out[dst_row_start..dst_row_start + copy_per_row]
9006 .copy_from_slice(&inp[src_row_start..src_row_start + copy_per_row]);
9007 }
9008 }
9009}
9010
9011fn is_trailing_bias_broadcast(rhs_dims: &[rlx_ir::Dim], out_dims: &[rlx_ir::Dim]) -> bool {
9029 if rhs_dims.len() > out_dims.len() {
9030 return false;
9031 }
9032 let off = out_dims.len() - rhs_dims.len();
9033 for i in 0..rhs_dims.len() {
9034 let r = match rhs_dims[i] {
9035 rlx_ir::Dim::Static(n) => n,
9036 _ => return false,
9037 };
9038 let o = match out_dims[off + i] {
9039 rlx_ir::Dim::Static(n) => n,
9040 _ => return false,
9041 };
9042 if r != o {
9043 return false;
9044 }
9045 }
9046 true
9047}
9048
9049fn broadcast_strides(in_dims: &[usize], out_dims: &[usize]) -> Vec<u32> {
9050 let r_out = out_dims.len();
9051 let r_in = in_dims.len();
9052 assert!(
9053 r_in <= r_out,
9054 "broadcast: input rank {r_in} > output rank {r_out}"
9055 );
9056 let pad = r_out - r_in;
9057 let mut strides = vec![0u32; r_out];
9058 let mut acc: usize = 1;
9059 for d in (0..r_out).rev() {
9060 let in_size = if d < pad { 1 } else { in_dims[d - pad] };
9061 if in_size == 1 {
9062 strides[d] = 0;
9063 } else {
9064 assert_eq!(
9065 in_size, out_dims[d],
9066 "broadcast: input dim {in_size} doesn't match output dim {} at axis {d}",
9067 out_dims[d]
9068 );
9069 strides[d] = acc as u32;
9070 acc *= in_size;
9071 }
9072 }
9073 strides
9074}
9075
9076pub fn execute_compiled(schedule: &ThunkSchedule, arena_buf: &mut [u8]) {
9080 let base = arena_buf.as_mut_ptr();
9081 for f in &schedule.compiled_fns {
9082 f(base);
9083 }
9084}
9085
9086pub fn execute_thunks_active(
9091 schedule: &ThunkSchedule,
9092 _arena_buf: &mut [u8],
9093 _actual: usize,
9094 _upper: usize,
9095) -> bool {
9096 let _ = schedule;
9097 false
9098}
9099
9100struct MoeResidencyGuard;
9102impl Drop for MoeResidencyGuard {
9103 fn drop(&mut self) {
9104 if let Some(stats) = crate::moe_residency::take_stats() {
9105 crate::moe_residency::stash_last_forward_stats(stats);
9106 } else {
9107 crate::moe_residency::clear_mask();
9108 }
9109 }
9110}
9111
9112fn thunk_kind_name(t: &Thunk) -> &'static str {
9113 match t {
9114 Thunk::Nop => "Nop",
9115 Thunk::Gather { .. } => "Gather",
9116 Thunk::GatherAxis { .. } => "GatherAxis",
9117 Thunk::TopK { .. } => "TopK",
9118 Thunk::Copy { .. } => "Copy",
9119 Thunk::CopyF64 { .. } => "CopyF64",
9120 Thunk::CopyI64 { .. } => "CopyI64",
9121 Thunk::CastF32ToI64 { .. } => "CastF32ToI64",
9122 Thunk::CastI64ToF32 { .. } => "CastI64ToF32",
9123 Thunk::CastBoolToI32 { .. } => "CastBoolToI32",
9124 Thunk::CastBoolToF32 { .. } => "CastBoolToF32",
9125 Thunk::CastI32ToF32 { .. } => "CastI32ToF32",
9126 Thunk::Transpose { .. } => "Transpose",
9127 Thunk::TransposeF64 { .. } => "TransposeF64",
9128 Thunk::Where { .. } => "Where",
9129 Thunk::Fma { .. } => "Fma",
9130 Thunk::Compare { .. } => "Compare",
9131 Thunk::BinaryFull { .. } => "BinaryFull",
9132 Thunk::BinaryFullF64 { .. } => "BinaryFullF64",
9133 Thunk::Sgemm { .. } => "Sgemm",
9134 Thunk::SgemmT { .. } => "SgemmT",
9135 Thunk::SgdMomentum { .. } => "SgdMomentum",
9136 Thunk::Dgemm { .. } => "Dgemm",
9137 Thunk::FusedMmBiasAct { .. } => "FusedMmBiasAct",
9138 Thunk::BiasAdd { .. } => "BiasAdd",
9139 Thunk::LayerNorm { .. } => "LayerNorm",
9140 Thunk::Softmax { .. } => "Softmax",
9141 Thunk::Conv2D { .. } => "Conv2D",
9142 Thunk::Conv2D1x1 { .. } => "Conv2D1x1",
9143 Thunk::CustomOp { .. } => "CustomOp",
9144 Thunk::ActivationInPlace { .. } => "ActivationInPlace",
9145 Thunk::Narrow { .. } => "Narrow",
9146 Thunk::Cumsum { .. } => "Cumsum",
9147 Thunk::Reduce { .. } => "Reduce",
9148 Thunk::BatchedSgemm { .. } => "BatchedSgemm",
9149 Thunk::DequantMatMul { .. } => "DequantMatMul",
9150 Thunk::Quantize { .. } => "Quantize",
9151 Thunk::Dequantize { .. } => "Dequantize",
9152 Thunk::ConvTranspose2d { .. } => "ConvTranspose2d",
9153 Thunk::ResizeNearest2x { .. } => "ResizeNearest2x",
9154 Thunk::ElementwiseRegion { .. } => "ElementwiseRegion",
9155 Thunk::Conv2dBackwardInput { .. } => "Conv2dBackwardInput",
9156 Thunk::Conv2dBackwardWeight { .. } => "Conv2dBackwardWeight",
9157 Thunk::Pool2D { .. } => "Pool2D",
9158 Thunk::MaxPool2dBackward { .. } => "MaxPool2dBackward",
9159 Thunk::ReluBackward { .. } => "ReluBackward",
9160 Thunk::ActivationBackward { .. } => "ActivationBackward",
9161 Thunk::Im2Col { .. } => "Im2Col",
9162 Thunk::SoftmaxCrossEntropyDense { .. } => "SoftmaxCrossEntropyDense",
9163 Thunk::SoftmaxCrossEntropy { .. } => "SoftmaxCrossEntropy",
9164 Thunk::SoftmaxCrossEntropyBackward { .. } => "SoftmaxCrossEntropyBackward",
9165 _ => "Other",
9166 }
9167}
9168
9169static THUNK_PROFILE: std::sync::Mutex<
9173 Option<std::collections::BTreeMap<&'static str, (u128, u64)>>,
9174> = std::sync::Mutex::new(None);
9175
9176#[inline]
9177fn profile_record(name: &'static str, d: std::time::Duration) {
9178 let mut g = THUNK_PROFILE.lock().unwrap();
9179 let map = g.get_or_insert_with(std::collections::BTreeMap::new);
9180 let e = map.entry(name).or_insert((0, 0));
9181 e.0 += d.as_nanos();
9182 e.1 += 1;
9183}
9184
9185pub fn dump_thunk_profile() {
9188 let mut g = THUNK_PROFILE.lock().unwrap();
9189 if let Some(map) = g.take() {
9190 let mut v: Vec<_> = map.into_iter().collect();
9191 v.sort_by_key(|b| std::cmp::Reverse(b.1.0));
9192 let total: u128 = v.iter().map(|(_, (ns, _))| *ns).sum();
9193 eprintln!(
9194 "[thunk-profile] total {:.1}ms across kinds:",
9195 total as f64 / 1e6
9196 );
9197 for (name, (ns, c)) in v.iter().take(25) {
9198 eprintln!(" {name:<28} {:>8.1}ms ({c} calls)", *ns as f64 / 1e6);
9199 }
9200 }
9201}
9202
9203pub fn execute_thunks(schedule: &ThunkSchedule, arena_buf: &mut [u8]) {
9204 crate::moe_residency::reset_gmm_counters();
9205 if let Some(layers) = schedule.moe_resident_layers.clone() {
9206 crate::moe_residency::set_per_layer_masks(Some(layers));
9207 } else {
9208 crate::moe_residency::set_mask(schedule.moe_resident.clone());
9209 }
9210 if let Some(cap) = schedule.moe_topk_capture.as_ref() {
9211 cap.clear();
9212 }
9213 let _moe_guard = MoeResidencyGuard;
9214 let base = arena_buf.as_mut_ptr();
9215 let mask_thr = schedule.mask_threshold;
9216 let mask_neg = schedule.mask_neg_inf;
9217 let score_thr = schedule.score_skip;
9218 let thunks = &schedule.thunks;
9219 let len = thunks.len();
9220
9221 let max_h = thunks
9223 .iter()
9224 .filter_map(|t| match t {
9225 Thunk::FusedResidualLN { h, .. }
9226 | Thunk::FusedResidualRmsNorm { h, .. }
9227 | Thunk::LayerNorm { h, .. } => Some(*h as usize),
9228 _ => None,
9229 })
9230 .max()
9231 .unwrap_or(0);
9232 let zero_bias = vec![0f32; max_h];
9233
9234 let max_sdpa = thunks
9237 .iter()
9238 .filter_map(|t| match t {
9239 Thunk::Attention {
9240 batch,
9241 seq,
9242 kv_seq,
9243 heads,
9244 head_dim,
9245 ..
9246 } => Some((
9247 *batch as usize,
9248 (*seq as usize).max(*kv_seq as usize),
9249 *heads as usize,
9250 *head_dim as usize,
9251 )),
9252 _ => None,
9253 })
9254 .fold((0, 0, 0, 0), |(mb, ms, mh, md), (b, s, h, d)| {
9255 (mb.max(b), ms.max(s), mh.max(h), md.max(d))
9256 });
9257 let (max_batch, max_seq, max_heads, _max_dh) = max_sdpa;
9258 let max_units = max_batch * max_heads;
9259 let mut sdpa_scores = vec![0f32; max_units * max_seq * max_seq];
9260
9261 let fl = thunks
9263 .iter()
9264 .filter_map(|t| match t {
9265 Thunk::FusedBertLayer {
9266 batch,
9267 seq,
9268 hs,
9269 int_dim,
9270 ..
9271 } => {
9272 let m = (*batch as usize) * (*seq as usize);
9273 let h = *hs as usize;
9274 let id = *int_dim as usize;
9275 Some((m, h, id, m * (*seq as usize)))
9276 }
9277 Thunk::FusedNomicLayer {
9278 batch,
9279 seq,
9280 hs,
9281 int_dim,
9282 ..
9283 } => {
9284 let m = (*batch as usize) * (*seq as usize);
9285 let h = *hs as usize;
9286 let id = *int_dim as usize;
9287 Some((m, h, id, m * (*seq as usize)))
9288 }
9289 _ => None,
9290 })
9291 .fold((0, 0, 0, 0), |(mm, mh, mi, ms), (m, h, id, ss)| {
9292 (mm.max(m), mh.max(h), mi.max(id), ms.max(ss))
9293 });
9294 let (fl_m, fl_h, fl_int, fl_ss) = fl;
9295 let mut fl_qkv = vec![0f32; fl_m * 3 * fl_h];
9296 let mut fl_attn = vec![0f32; fl_m * fl_h];
9297 let mut fl_res = vec![0f32; fl_m * fl_h];
9298 let mut fl_normed = vec![0f32; fl_m * fl_h];
9299 let mut fl_ffn = vec![0f32; fl_m * fl_int.max(2 * fl_int)]; let mut fl_sc = vec![0f32; fl_ss.max(1)];
9301
9302 let trace_thunks = std::env::var_os("RLX_TRACE_THUNK").is_some();
9303 if trace_thunks {
9304 eprintln!(
9305 "[thunk] prealloc max_h={max_h} sdpa={} fl_m={fl_m} fl_h={fl_h} fl_int={fl_int}",
9306 max_units * max_seq * max_seq
9307 );
9308 }
9309 let profile = std::env::var_os("RLX_PROFILE_THUNKS").is_some();
9310 let mut prof_prev: Option<(&'static str, std::time::Instant)> = None;
9314 for i in 0..len {
9315 if profile {
9316 if let Some((pn, pt)) = prof_prev.take() {
9317 profile_record(pn, pt.elapsed());
9318 }
9319 }
9320 let thunk = unsafe { thunks.get_unchecked(i) };
9321 if trace_thunks && (i < 120 || i % 200 == 0 || i + 1 == len) {
9322 eprintln!("[thunk {i}/{len}] {}", thunk_kind_name(thunk));
9323 }
9324 let trace_done = trace_thunks && i < 120;
9325 if profile {
9326 prof_prev = Some((thunk_kind_name(thunk), std::time::Instant::now()));
9327 }
9328 match thunk {
9329 Thunk::Nop => {}
9330
9331 Thunk::ElementwiseRegion {
9332 dst,
9333 len,
9334 input_offs,
9335 chain,
9336 scalar_input_mask,
9337 input_modulus,
9338 } => {
9339 let len = *len as usize;
9340 if !chain.is_empty() && len > 0 {
9341 let base_addr = base as usize;
9342 let dst = *dst;
9343 let scalar_mask = *scalar_input_mask;
9344 let eval = |gid: usize| {
9346 let v = region_eval_elem(
9347 gid,
9348 base_addr as *const u8,
9349 input_offs,
9350 chain,
9351 scalar_mask,
9352 input_modulus,
9353 );
9354 unsafe {
9355 *((base_addr as *mut u8).add(dst) as *mut f32).add(gid) = v;
9356 }
9357 };
9358 if fast_conv_enabled() && crate::pool::should_parallelize(len) {
9359 crate::pool::par_for(len, crate::pool::chunk_floor(len), &|off, cnt| {
9360 for gid in off..off + cnt {
9361 eval(gid);
9362 }
9363 });
9364 } else {
9365 for gid in 0..len {
9366 eval(gid);
9367 }
9368 }
9369 }
9370 }
9371
9372 Thunk::GaussianSplatRender {
9373 positions_off,
9374 positions_len,
9375 scales_off,
9376 scales_len,
9377 rotations_off,
9378 rotations_len,
9379 opacities_off,
9380 opacities_len,
9381 colors_off,
9382 colors_len,
9383 sh_coeffs_off,
9384 sh_coeffs_len,
9385 meta_off,
9386 dst_off,
9387 dst_len,
9388 width,
9389 height,
9390 tile_size,
9391 radius_scale,
9392 alpha_cutoff,
9393 max_splat_steps,
9394 transmittance_threshold,
9395 max_list_entries,
9396 } => unsafe {
9397 crate::splat::execute_gaussian_splat_render(
9398 *positions_off,
9399 *positions_len,
9400 *scales_off,
9401 *scales_len,
9402 *rotations_off,
9403 *rotations_len,
9404 *opacities_off,
9405 *opacities_len,
9406 *colors_off,
9407 *colors_len,
9408 *sh_coeffs_off,
9409 *sh_coeffs_len,
9410 *meta_off,
9411 *dst_off,
9412 *dst_len,
9413 *width,
9414 *height,
9415 *tile_size,
9416 *radius_scale,
9417 *alpha_cutoff,
9418 *max_splat_steps,
9419 *transmittance_threshold,
9420 *max_list_entries,
9421 base,
9422 );
9423 },
9424
9425 Thunk::GaussianSplatRenderBackward {
9426 positions_off,
9427 positions_len,
9428 scales_off,
9429 scales_len,
9430 rotations_off,
9431 rotations_len,
9432 opacities_off,
9433 opacities_len,
9434 colors_off,
9435 colors_len,
9436 sh_coeffs_off,
9437 sh_coeffs_len,
9438 meta_off,
9439 d_loss_off,
9440 d_loss_len,
9441 packed_off,
9442 packed_len,
9443 width,
9444 height,
9445 tile_size,
9446 radius_scale,
9447 alpha_cutoff,
9448 max_splat_steps,
9449 transmittance_threshold,
9450 max_list_entries,
9451 loss_grad_clip,
9452 sh_band,
9453 max_anisotropy,
9454 } => unsafe {
9455 crate::splat::execute_gaussian_splat_render_backward(
9456 *positions_off,
9457 *positions_len,
9458 *scales_off,
9459 *scales_len,
9460 *rotations_off,
9461 *rotations_len,
9462 *opacities_off,
9463 *opacities_len,
9464 *colors_off,
9465 *colors_len,
9466 *sh_coeffs_off,
9467 *sh_coeffs_len,
9468 *meta_off,
9469 *d_loss_off,
9470 *d_loss_len,
9471 *packed_off,
9472 *packed_len,
9473 *width,
9474 *height,
9475 *tile_size,
9476 *radius_scale,
9477 *alpha_cutoff,
9478 *max_splat_steps,
9479 *transmittance_threshold,
9480 *max_list_entries,
9481 *loss_grad_clip,
9482 *sh_band,
9483 *max_anisotropy,
9484 base,
9485 );
9486 },
9487
9488 Thunk::GaussianSplatPrepare {
9489 positions_off,
9490 positions_len,
9491 scales_off,
9492 scales_len,
9493 rotations_off,
9494 rotations_len,
9495 opacities_off,
9496 opacities_len,
9497 colors_off,
9498 colors_len,
9499 sh_coeffs_off,
9500 sh_coeffs_len,
9501 meta_off,
9502 meta_len,
9503 prep_off,
9504 prep_len,
9505 width,
9506 height,
9507 tile_size,
9508 radius_scale,
9509 alpha_cutoff,
9510 max_splat_steps,
9511 transmittance_threshold,
9512 max_list_entries,
9513 } => unsafe {
9514 crate::splat::execute_gaussian_splat_prepare(
9515 *positions_off,
9516 *positions_len,
9517 *scales_off,
9518 *scales_len,
9519 *rotations_off,
9520 *rotations_len,
9521 *opacities_off,
9522 *opacities_len,
9523 *colors_off,
9524 *colors_len,
9525 *sh_coeffs_off,
9526 *sh_coeffs_len,
9527 *meta_off,
9528 *meta_len,
9529 *prep_off,
9530 *prep_len,
9531 *width,
9532 *height,
9533 *tile_size,
9534 *radius_scale,
9535 *alpha_cutoff,
9536 *max_splat_steps,
9537 *transmittance_threshold,
9538 *max_list_entries,
9539 base,
9540 );
9541 },
9542
9543 Thunk::GaussianSplatRasterize {
9544 prep_off,
9545 prep_len,
9546 meta_off,
9547 meta_len,
9548 dst_off,
9549 dst_len,
9550 count,
9551 width,
9552 height,
9553 tile_size,
9554 alpha_cutoff,
9555 max_splat_steps,
9556 transmittance_threshold,
9557 max_list_entries,
9558 } => unsafe {
9559 crate::splat::execute_gaussian_splat_rasterize(
9560 *prep_off,
9561 *prep_len,
9562 *meta_off,
9563 *meta_len,
9564 *dst_off,
9565 *dst_len,
9566 *count,
9567 *width,
9568 *height,
9569 *tile_size,
9570 *alpha_cutoff,
9571 *max_splat_steps,
9572 *transmittance_threshold,
9573 *max_list_entries,
9574 base,
9575 );
9576 },
9577
9578 Thunk::Fft1d {
9579 src,
9580 dst,
9581 outer,
9582 n_complex,
9583 inverse,
9584 norm_tag,
9585 dtype,
9586 } => unsafe {
9587 match dtype {
9588 rlx_ir::DType::F64 => execute_fft1d_f64(
9589 *src,
9590 *dst,
9591 *outer as usize,
9592 *n_complex as usize,
9593 *inverse,
9594 *norm_tag,
9595 base,
9596 ),
9597 rlx_ir::DType::F32 => execute_fft1d_f32(
9598 *src,
9599 *dst,
9600 *outer as usize,
9601 *n_complex as usize,
9602 *inverse,
9603 *norm_tag,
9604 base,
9605 ),
9606 rlx_ir::DType::C64 => execute_fft1d_c64(
9607 *src,
9608 *dst,
9609 *outer as usize,
9610 *n_complex as usize,
9611 *inverse,
9612 *norm_tag,
9613 base,
9614 ),
9615 other => panic!("Op::Fft on CPU requires F32/F64/C64, got {other:?}"),
9616 }
9617 },
9618
9619 Thunk::FftButterflyStage {
9620 state_src,
9621 state_dst,
9622 gate_src,
9623 rev_src,
9624 tw_re_src,
9625 tw_im_src,
9626 batch,
9627 n_fft,
9628 stage,
9629 } => unsafe {
9630 execute_fft_butterfly_stage_f32(
9631 *state_src,
9632 *state_dst,
9633 *gate_src,
9634 *rev_src,
9635 *tw_re_src,
9636 *tw_im_src,
9637 *batch as usize,
9638 *n_fft as usize,
9639 *stage as usize,
9640 base,
9641 );
9642 },
9643
9644 Thunk::LogMel {
9645 spec,
9646 filters,
9647 dst,
9648 outer,
9649 n_fft,
9650 n_bins,
9651 n_mels,
9652 } => unsafe {
9653 execute_log_mel_f32(
9654 *spec,
9655 *filters,
9656 *dst,
9657 *outer as usize,
9658 *n_fft as usize,
9659 *n_bins as usize,
9660 *n_mels as usize,
9661 base,
9662 );
9663 },
9664
9665 Thunk::LogMelBackward {
9666 spec,
9667 filters,
9668 dy,
9669 dst,
9670 outer,
9671 n_fft,
9672 n_bins,
9673 n_mels,
9674 } => unsafe {
9675 execute_log_mel_backward_f32(
9676 *spec,
9677 *filters,
9678 *dy,
9679 *dst,
9680 *outer as usize,
9681 *n_fft as usize,
9682 *n_bins as usize,
9683 *n_mels as usize,
9684 base,
9685 );
9686 },
9687
9688 Thunk::WelchPeaks {
9689 spec,
9690 dst,
9691 welch_batch,
9692 n_fft,
9693 n_segments,
9694 k,
9695 } => unsafe {
9696 execute_welch_peaks_f32(
9697 *spec,
9698 *dst,
9699 *welch_batch as usize,
9700 *n_fft as usize,
9701 *n_segments as usize,
9702 *k as usize,
9703 base,
9704 );
9705 },
9706
9707 Thunk::CustomFn {
9711 body,
9712 body_init,
9713 inputs,
9714 body_output_off,
9715 outer_output_off,
9716 out_bytes,
9717 } => {
9718 let mut body_buf: Vec<u8> = (**body_init).clone();
9719 unsafe {
9720 for (body_in_off, outer_in_off, n_bytes) in inputs.iter() {
9721 let src = (base as *const u8).add(*outer_in_off);
9722 let dst = body_buf.as_mut_ptr().add(*body_in_off);
9723 std::ptr::copy_nonoverlapping(src, dst, *n_bytes as usize);
9724 }
9725 }
9726 execute_thunks(body, &mut body_buf);
9727 unsafe {
9728 let src = body_buf.as_ptr().add(*body_output_off);
9729 let dst = base.add(*outer_output_off);
9730 std::ptr::copy_nonoverlapping(src, dst, *out_bytes as usize);
9731 }
9732 }
9733
9734 Thunk::Sgemm { a, b, c, m, k, n } => {
9735 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
9736 if trace_thunks {
9737 eprintln!("[sgemm] m={m} k={k} n={n} a={} b={} c={}", *a, *b, *c);
9738 }
9739 let c_len = m.saturating_mul(n);
9740 let a_len = m.saturating_mul(k);
9741 let b_len = k.saturating_mul(n);
9742 let arena_len = arena_buf.len();
9743 let max_a = (arena_len.saturating_sub(*a)) / 4;
9744 let max_b = (arena_len.saturating_sub(*b)) / 4;
9745 let max_c = (arena_len.saturating_sub(*c)) / 4;
9746 let a_len = a_len.min(max_a);
9747 let b_len = b_len.min(max_b);
9748 let c_len = c_len.min(max_c);
9749 unsafe {
9750 let a_sl = sl(*a, base, a_len);
9751 let b_sl = sl(*b, base, b_len);
9752 let c_sl = sl_mut(*c, base, c_len);
9753 if std::ptr::eq(a_sl.as_ptr(), c_sl.as_ptr())
9754 || std::ptr::eq(b_sl.as_ptr(), c_sl.as_ptr())
9755 {
9756 let mut tmp = vec![0.0f32; c_len];
9757 crate::blas::sgemm_auto(a_sl, b_sl, &mut tmp, m, k, n);
9758 c_sl.copy_from_slice(&tmp);
9759 } else {
9760 crate::blas::sgemm_auto(a_sl, b_sl, c_sl, m, k, n);
9761 }
9762 }
9763 }
9764
9765 Thunk::SgemmT {
9766 a,
9767 b,
9768 c,
9769 m,
9770 k,
9771 n,
9772 ta,
9773 tb,
9774 } => {
9775 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
9780 let lda = if *ta { m } else { k };
9781 let ldb = if *tb { k } else { n };
9782 let arena_len = arena_buf.len();
9783 let a_len = (m * k).min((arena_len.saturating_sub(*a)) / 4);
9784 let b_len = (k * n).min((arena_len.saturating_sub(*b)) / 4);
9785 let c_len = (m * n).min((arena_len.saturating_sub(*c)) / 4);
9786 unsafe {
9787 let a_sl = sl(*a, base, a_len);
9788 let b_sl = sl(*b, base, b_len);
9789 let c_sl = sl_mut(*c, base, c_len);
9790 let (ap, bp) = (a_sl.as_ptr(), b_sl.as_ptr());
9791 if std::ptr::eq(ap, c_sl.as_ptr()) || std::ptr::eq(bp, c_sl.as_ptr()) {
9792 let mut tmp = vec![0.0f32; c_len];
9793 crate::blas::sgemm_general(
9794 ap,
9795 bp,
9796 tmp.as_mut_ptr(),
9797 m,
9798 n,
9799 k,
9800 1.0,
9801 0.0,
9802 lda,
9803 ldb,
9804 n,
9805 *ta,
9806 *tb,
9807 );
9808 c_sl.copy_from_slice(&tmp);
9809 } else {
9810 crate::blas::sgemm_general(
9811 ap,
9812 bp,
9813 c_sl.as_mut_ptr(),
9814 m,
9815 n,
9816 k,
9817 1.0,
9818 0.0,
9819 lda,
9820 ldb,
9821 n,
9822 *ta,
9823 *tb,
9824 );
9825 }
9826 }
9827 }
9828
9829 Thunk::SgdMomentum {
9830 param,
9831 vel,
9832 grad,
9833 p_out,
9834 v_out,
9835 lr,
9836 mom,
9837 len,
9838 } => {
9839 let len = *len as usize;
9844 let (lr, mom) = (*lr, *mom);
9845 unsafe {
9846 let p = sl(*param, base, len);
9847 let v = sl(*vel, base, len);
9848 let g = sl(*grad, base, len);
9849 let po = sl_mut(*p_out, base, len);
9850 let vo = sl_mut(*v_out, base, len);
9851 if fast_conv_enabled() && crate::pool::should_parallelize(len) {
9852 let poa = po.as_mut_ptr() as usize;
9853 let voa = vo.as_mut_ptr() as usize;
9854 crate::pool::par_for(len, crate::pool::chunk_floor(len), &|off, cnt| {
9855 for i in off..off + cnt {
9856 let vn = mom * v[i] + g[i];
9857 *((voa as *mut f32).add(i)) = vn;
9858 *((poa as *mut f32).add(i)) = p[i] - lr * vn;
9859 }
9860 });
9861 } else {
9862 for i in 0..len {
9863 let vn = mom * v[i] + g[i];
9864 vo[i] = vn;
9865 po[i] = p[i] - lr * vn;
9866 }
9867 }
9868 }
9869 }
9870
9871 Thunk::CgemmC64 { a, b, c, m, k, n } => unsafe {
9872 cgemm_c64(*a, *b, *c, *m as usize, *k as usize, *n as usize, base);
9873 },
9874
9875 Thunk::DenseSolveF64 { a, b, x, n, nrhs } => {
9876 let (n_, nrhs_) = (*n as usize, *nrhs as usize);
9877 unsafe {
9883 let a_src = sl_f64(*a, base, n_ * n_);
9884 let b_src = sl_f64(*b, base, n_ * nrhs_);
9885 let mut a_scratch: Vec<f64> = a_src.to_vec();
9886 let mut x_buf: Vec<f64> = b_src.to_vec();
9887 let info = crate::blas::dgesv(&mut a_scratch, &mut x_buf, n_, nrhs_);
9888 if info != 0 {
9889 panic!(
9890 "DenseSolveF64: dgesv reported singular matrix \
9891 (info={info}, n={n_}, nrhs={nrhs_})"
9892 );
9893 }
9894 let dst = sl_mut_f64(*x, base, n_ * nrhs_);
9895 dst.copy_from_slice(&x_buf);
9896 }
9897 }
9898
9899 Thunk::DenseSolveF32 { a, b, x, n, nrhs } => {
9900 let (n_, nrhs_) = (*n as usize, *nrhs as usize);
9901 unsafe {
9902 let a_src = sl(*a, base, n_ * n_);
9903 let b_src = sl(*b, base, n_ * nrhs_);
9904 let mut a_scratch: Vec<f32> = a_src.to_vec();
9905 let mut x_buf: Vec<f32> = b_src.to_vec();
9906 let info = crate::blas::sgesv(&mut a_scratch, &mut x_buf, n_, nrhs_);
9907 if info != 0 {
9908 panic!(
9909 "DenseSolveF32: sgesv reported singular matrix \
9910 (info={info}, n={n_}, nrhs={nrhs_})"
9911 );
9912 }
9913 let dst = sl_mut(*x, base, n_ * nrhs_);
9914 dst.copy_from_slice(&x_buf);
9915 }
9916 }
9917
9918 Thunk::BatchedDenseSolveF64 {
9919 a,
9920 b,
9921 x,
9922 batch,
9923 n,
9924 nrhs,
9925 } => {
9926 let (b_, n_, nrhs_) = (*batch as usize, *n as usize, *nrhs as usize);
9933 let a_stride = n_ * n_;
9934 let b_stride = n_ * nrhs_;
9935 unsafe {
9936 let a_full = sl_f64(*a, base, b_ * a_stride);
9937 let b_full = sl_f64(*b, base, b_ * b_stride);
9938 let x_full = sl_mut_f64(*x, base, b_ * b_stride);
9939 for bi in 0..b_ {
9940 let mut a_scratch: Vec<f64> =
9941 a_full[bi * a_stride..(bi + 1) * a_stride].to_vec();
9942 let mut x_buf: Vec<f64> =
9943 b_full[bi * b_stride..(bi + 1) * b_stride].to_vec();
9944 let info = crate::blas::dgesv(&mut a_scratch, &mut x_buf, n_, nrhs_);
9945 if info != 0 {
9946 panic!(
9947 "BatchedDenseSolveF64: slice {bi} \
9948 singular (info={info}, n={n_}, nrhs={nrhs_})"
9949 );
9950 }
9951 x_full[bi * b_stride..(bi + 1) * b_stride].copy_from_slice(&x_buf);
9952 }
9953 }
9954 }
9955
9956 Thunk::BatchedDenseSolveF32 {
9957 a,
9958 b,
9959 x,
9960 batch,
9961 n,
9962 nrhs,
9963 } => {
9964 let (b_, n_, nrhs_) = (*batch as usize, *n as usize, *nrhs as usize);
9965 let a_stride = n_ * n_;
9966 let b_stride = n_ * nrhs_;
9967 unsafe {
9968 let a_full = sl(*a, base, b_ * a_stride);
9969 let b_full = sl(*b, base, b_ * b_stride);
9970 let x_full = sl_mut(*x, base, b_ * b_stride);
9971 for bi in 0..b_ {
9972 let mut a_scratch = a_full[bi * a_stride..(bi + 1) * a_stride].to_vec();
9973 let mut x_buf = b_full[bi * b_stride..(bi + 1) * b_stride].to_vec();
9974 let info = crate::blas::sgesv(&mut a_scratch, &mut x_buf, n_, nrhs_);
9975 if info != 0 {
9976 panic!("BatchedDenseSolveF32: slice {bi} singular (info={info})");
9977 }
9978 x_full[bi * b_stride..(bi + 1) * b_stride].copy_from_slice(&x_buf);
9979 }
9980 }
9981 }
9982
9983 Thunk::BatchedDgemmF64 {
9984 a,
9985 b,
9986 c,
9987 batch,
9988 m,
9989 k,
9990 n,
9991 } => {
9992 let (b_, m_, k_, n_) = (*batch as usize, *m as usize, *k as usize, *n as usize);
9993 let a_stride = m_ * k_;
9994 let b_stride = k_ * n_;
9995 let c_stride = m_ * n_;
9996 unsafe {
9997 let a_full = sl_f64(*a, base, b_ * a_stride);
9998 let b_full = sl_f64(*b, base, b_ * b_stride);
9999 let c_full = sl_mut_f64(*c, base, b_ * c_stride);
10000 for bi in 0..b_ {
10001 let a_slice = &a_full[bi * a_stride..(bi + 1) * a_stride];
10002 let b_slice = &b_full[bi * b_stride..(bi + 1) * b_stride];
10003 let c_slice = &mut c_full[bi * c_stride..(bi + 1) * c_stride];
10004 crate::blas::dgemm(a_slice, b_slice, c_slice, m_, k_, n_);
10005 }
10006 }
10007 }
10008
10009 Thunk::BatchedSgemm {
10010 a,
10011 b,
10012 c,
10013 batch,
10014 m,
10015 k,
10016 n,
10017 } => {
10018 let (b_, m_, k_, n_) = (*batch as usize, *m as usize, *k as usize, *n as usize);
10019 if trace_thunks {
10020 eprintln!(
10021 "[batched-sgemm] batch={b_} m={m_} k={k_} n={n_} a={} b={} c={}",
10022 *a, *b, *c
10023 );
10024 }
10025 let a_stride = m_.saturating_mul(k_);
10026 let b_stride = k_.saturating_mul(n_);
10027 let c_stride = m_.saturating_mul(n_);
10028 let arena_len = arena_buf.len();
10029 let a_cap = (arena_len.saturating_sub(*a)) / 4;
10030 let b_cap = (arena_len.saturating_sub(*b)) / 4;
10031 let c_cap = (arena_len.saturating_sub(*c)) / 4;
10032 let a_elems = (b_ * a_stride).min(a_cap);
10033 let b_elems = (b_ * b_stride).min(b_cap);
10034 let c_elems = (b_ * c_stride).min(c_cap);
10035 let b_eff = b_
10036 .min(a_elems.checked_div(a_stride).unwrap_or(0))
10037 .min(b_elems.checked_div(b_stride).unwrap_or(0))
10038 .min(c_elems.checked_div(c_stride).unwrap_or(0));
10039 unsafe {
10040 let a_full = sl(*a, base, a_elems);
10041 let b_full = sl(*b, base, b_elems);
10042 let c_full = sl_mut(*c, base, c_elems);
10043 for bi in 0..b_eff {
10044 let a0 = bi * a_stride;
10045 let b0 = bi * b_stride;
10046 let c0 = bi * c_stride;
10047 if a0 + a_stride > a_full.len()
10048 || b0 + b_stride > b_full.len()
10049 || c0 + c_stride > c_full.len()
10050 {
10051 break;
10052 }
10053 let a_slice = &a_full[a0..a0 + a_stride];
10054 let b_slice = &b_full[b0..b0 + b_stride];
10055 let c_slice = &mut c_full[c0..c0 + c_stride];
10056 if std::ptr::eq(a_slice.as_ptr(), c_slice.as_mut_ptr())
10057 || std::ptr::eq(b_slice.as_ptr(), c_slice.as_mut_ptr())
10058 {
10059 let mut tmp = vec![0.0f32; c_stride];
10060 crate::blas::sgemm_auto(a_slice, b_slice, &mut tmp, m_, k_, n_);
10061 c_slice.copy_from_slice(&tmp);
10062 } else {
10063 crate::blas::sgemm_auto(a_slice, b_slice, c_slice, m_, k_, n_);
10064 }
10065 }
10066 }
10067 }
10068
10069 Thunk::Dgemm { a, b, c, m, k, n } => {
10070 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
10071 unsafe {
10072 crate::blas::dgemm(
10073 sl_f64(*a, base, m * k),
10074 sl_f64(*b, base, k * n),
10075 sl_mut_f64(*c, base, m * n),
10076 m,
10077 k,
10078 n,
10079 );
10080 }
10081 }
10082
10083 Thunk::TransposeF64 {
10084 src,
10085 dst,
10086 in_total,
10087 out_dims,
10088 in_strides,
10089 } => unsafe {
10090 let inp = sl_f64(*src, base, *in_total as usize);
10091 let out_total: usize = out_dims.iter().map(|d| *d as usize).product();
10092 let out = sl_mut_f64(*dst, base, out_total);
10093 transpose_walk_f64(inp, out, out_dims, in_strides);
10094 },
10095
10096 Thunk::ActivationF64 {
10097 src,
10098 dst,
10099 len,
10100 kind,
10101 } => {
10102 let len = *len as usize;
10103 unsafe {
10104 let inp = sl_f64(*src, base, len);
10105 let out = sl_mut_f64(*dst, base, len);
10106 apply_activation_f64(inp, out, *kind);
10107 }
10108 }
10109
10110 Thunk::ReduceSumF64 {
10111 src,
10112 dst,
10113 outer,
10114 reduced,
10115 inner,
10116 } => {
10117 let (o, r, n) = (*outer as usize, *reduced as usize, *inner as usize);
10118 unsafe {
10119 let inp = sl_f64(*src, base, o * r * n);
10120 let out = sl_mut_f64(*dst, base, o * n);
10121 reduce_sum_f64(inp, out, o, r, n);
10122 }
10123 }
10124
10125 Thunk::CopyF64 { src, dst, len } => {
10126 let mut len = *len as usize;
10127 if *src == *dst || len == 0 {
10128 continue;
10129 }
10130 let arena_len = arena_buf.len();
10131 let max_from_src = (arena_len.saturating_sub(*src)) / 8;
10132 let max_from_dst = (arena_len.saturating_sub(*dst)) / 8;
10133 len = len.min(max_from_src).min(max_from_dst);
10134 if len == 0 {
10135 continue;
10136 }
10137 let byte_len = len.saturating_mul(8);
10138 unsafe {
10139 std::ptr::copy(base.add(*src), base.add(*dst), byte_len);
10140 }
10141 }
10142
10143 Thunk::CopyI64 { src, dst, len } => {
10144 let mut len = *len as usize;
10145 if *src == *dst || len == 0 {
10146 continue;
10147 }
10148 let arena_len = arena_buf.len();
10149 let max_from_src = (arena_len.saturating_sub(*src)) / 8;
10150 let max_from_dst = (arena_len.saturating_sub(*dst)) / 8;
10151 len = len.min(max_from_src).min(max_from_dst);
10152 if len == 0 {
10153 continue;
10154 }
10155 let byte_len = len.saturating_mul(8);
10156 unsafe {
10157 std::ptr::copy(base.add(*src), base.add(*dst), byte_len);
10158 }
10159 }
10160
10161 Thunk::CastF32ToI64 { src, dst, len } => {
10162 let len = *len as usize;
10163 if len == 0 {
10164 continue;
10165 }
10166 unsafe {
10167 let inp = sl(*src, base, len);
10168 let out = sl_mut_i64(*dst, base, len);
10169 for i in 0..len {
10170 out[i] = inp[i].round() as i64;
10171 }
10172 }
10173 }
10174
10175 Thunk::CastF32ToF64 { src, dst, len } => {
10176 let len = *len as usize;
10177 if len == 0 {
10178 continue;
10179 }
10180 unsafe {
10181 let inp = sl(*src, base, len);
10182 let out = sl_mut_f64(*dst, base, len);
10183 for i in 0..len {
10184 out[i] = inp[i] as f64;
10185 }
10186 }
10187 }
10188
10189 Thunk::CastF32ToI32 { src, dst, len } => {
10190 let len = *len as usize;
10191 if len == 0 {
10192 continue;
10193 }
10194 unsafe {
10195 let inp = sl(*src, base, len);
10196 let out = sl_mut_i32(*dst, base, len);
10197 for i in 0..len {
10198 out[i] = inp[i].round() as i32;
10199 }
10200 }
10201 }
10202
10203 Thunk::CastI64ToF32 { src, dst, len } => {
10204 let len = *len as usize;
10205 if len == 0 {
10206 continue;
10207 }
10208 unsafe {
10209 let inp = sl_i64(*src, base, len);
10210 let out = sl_mut(*dst, base, len);
10211 for i in 0..len {
10212 out[i] = inp[i] as f32;
10213 }
10214 }
10215 }
10216
10217 Thunk::CastBoolToI32 { src, dst, len } => {
10218 let len = *len as usize;
10219 if len == 0 {
10220 continue;
10221 }
10222 unsafe {
10223 let inp = &arena_buf[*src..*src + len];
10224 let out = sl_mut_i32(*dst, base, len);
10225 for i in 0..len {
10226 out[i] = i32::from(inp[i] != 0);
10227 }
10228 }
10229 }
10230
10231 Thunk::CastI32ToF32 { src, dst, len } => {
10232 let len = *len as usize;
10233 if len == 0 {
10234 continue;
10235 }
10236 unsafe {
10237 let inp = sl_i32(*src, base, len);
10238 let out = sl_mut(*dst, base, len);
10239 for i in 0..len {
10240 out[i] = inp[i] as f32;
10241 }
10242 }
10243 }
10244
10245 Thunk::CastBoolToF32 { src, dst, len } => {
10246 let len = *len as usize;
10247 if len == 0 {
10248 continue;
10249 }
10250 unsafe {
10251 let inp = &arena_buf[*src..*src + len];
10252 let out = sl_mut(*dst, base, len);
10253 for i in 0..len {
10254 out[i] = if inp[i] != 0 { 1.0 } else { 0.0 };
10255 }
10256 }
10257 }
10258
10259 Thunk::BinaryFullF64 {
10260 lhs,
10261 rhs,
10262 dst,
10263 len,
10264 lhs_len,
10265 rhs_len,
10266 op,
10267 out_dims_bcast,
10268 bcast_lhs_strides,
10269 bcast_rhs_strides,
10270 } => {
10271 let len = *len as usize;
10272 let lhs_len = *lhs_len as usize;
10273 let rhs_len = *rhs_len as usize;
10274 unsafe {
10275 let l = sl_f64(*lhs, base, lhs_len);
10276 let r = sl_f64(*rhs, base, rhs_len);
10277 let d = sl_mut_f64(*dst, base, len);
10278 if lhs_len == len && rhs_len == len {
10279 for i in 0..len {
10280 d[i] = binary_op_f64(*op, l[i], r[i]);
10281 }
10282 } else if !out_dims_bcast.is_empty() {
10283 let rank = out_dims_bcast.len();
10287 let mut coords = vec![0u32; rank];
10288 for i in 0..len {
10289 let mut rem = i;
10290 for ax in (0..rank).rev() {
10291 let sz = out_dims_bcast[ax] as usize;
10292 coords[ax] = (rem % sz) as u32;
10293 rem /= sz;
10294 }
10295 let mut li: usize = 0;
10296 let mut ri: usize = 0;
10297 for ax in 0..rank {
10298 li += coords[ax] as usize * bcast_lhs_strides[ax] as usize;
10299 ri += coords[ax] as usize * bcast_rhs_strides[ax] as usize;
10300 }
10301 d[i] = binary_op_f64(*op, l[li], r[ri]);
10302 }
10303 } else {
10304 for i in 0..len {
10309 d[i] = binary_op_f64(*op, l[i % lhs_len], r[i % rhs_len]);
10310 }
10311 }
10312 }
10313 }
10314
10315 Thunk::BinaryFullC64 {
10316 lhs,
10317 rhs,
10318 dst,
10319 len,
10320 lhs_len,
10321 rhs_len,
10322 op,
10323 out_dims_bcast,
10324 bcast_lhs_strides,
10325 bcast_rhs_strides,
10326 } => {
10327 let n_out = *len as usize;
10333 let n_l = *lhs_len as usize;
10334 let n_r = *rhs_len as usize;
10335 unsafe {
10336 let l = sl(*lhs, base, 2 * n_l);
10337 let r = sl(*rhs, base, 2 * n_r);
10338 let d = sl_mut(*dst, base, 2 * n_out);
10339 let do_c64 = |a_re: f32, a_im: f32, b_re: f32, b_im: f32| -> (f32, f32) {
10340 match op {
10341 BinaryOp::Add => (a_re + b_re, a_im + b_im),
10342 BinaryOp::Sub => (a_re - b_re, a_im - b_im),
10343 BinaryOp::Mul => (a_re * b_re - a_im * b_im, a_re * b_im + a_im * b_re),
10344 BinaryOp::Div => {
10345 let denom = b_re * b_re + b_im * b_im;
10346 (
10347 (a_re * b_re + a_im * b_im) / denom,
10348 (a_im * b_re - a_re * b_im) / denom,
10349 )
10350 }
10351 BinaryOp::Max | BinaryOp::Min | BinaryOp::Pow => {
10352 unreachable!("C64 max/min/pow rejected at lowering")
10353 }
10354 }
10355 };
10356 if n_l == n_out && n_r == n_out {
10357 for i in 0..n_out {
10358 let (re, im) = do_c64(l[2 * i], l[2 * i + 1], r[2 * i], r[2 * i + 1]);
10359 d[2 * i] = re;
10360 d[2 * i + 1] = im;
10361 }
10362 } else if !out_dims_bcast.is_empty() {
10363 let rank = out_dims_bcast.len();
10367 let mut coords = vec![0u32; rank];
10368 for i in 0..n_out {
10369 let mut rem = i;
10370 for ax in (0..rank).rev() {
10371 let sz = out_dims_bcast[ax] as usize;
10372 coords[ax] = (rem % sz) as u32;
10373 rem /= sz;
10374 }
10375 let mut li: usize = 0;
10376 let mut ri: usize = 0;
10377 for ax in 0..rank {
10378 li += coords[ax] as usize * bcast_lhs_strides[ax] as usize;
10379 ri += coords[ax] as usize * bcast_rhs_strides[ax] as usize;
10380 }
10381 let (re, im) =
10382 do_c64(l[2 * li], l[2 * li + 1], r[2 * ri], r[2 * ri + 1]);
10383 d[2 * i] = re;
10384 d[2 * i + 1] = im;
10385 }
10386 } else {
10387 for i in 0..n_out {
10389 let li = if n_l == 1 { 0 } else { i % n_l };
10390 let ri = if n_r == 1 { 0 } else { i % n_r };
10391 let (re, im) =
10392 do_c64(l[2 * li], l[2 * li + 1], r[2 * ri], r[2 * ri + 1]);
10393 d[2 * i] = re;
10394 d[2 * i + 1] = im;
10395 }
10396 }
10397 }
10398 }
10399
10400 Thunk::ComplexNormSqF32 { src, dst, len } => {
10401 let n = *len as usize;
10402 unsafe {
10403 let s = sl(*src, base, 2 * n);
10404 let d = sl_mut(*dst, base, n);
10405 for i in 0..n {
10406 let re = s[2 * i];
10407 let im = s[2 * i + 1];
10408 d[i] = re * re + im * im;
10409 }
10410 }
10411 }
10412
10413 Thunk::ComplexNormSqBackwardF32 { z, g, dz, len } => {
10414 let n = *len as usize;
10417 unsafe {
10418 let zb = sl(*z, base, 2 * n);
10419 let gb = sl(*g, base, n);
10420 let db = sl_mut(*dz, base, 2 * n);
10421 for i in 0..n {
10422 let re = zb[2 * i];
10423 let im = zb[2 * i + 1];
10424 let gv = gb[i];
10425 db[2 * i] = gv * re;
10426 db[2 * i + 1] = gv * im;
10427 }
10428 }
10429 }
10430
10431 Thunk::ConjugateC64 { src, dst, len } => {
10432 let n = *len as usize;
10433 unsafe {
10434 let s = sl(*src, base, 2 * n);
10435 let d = sl_mut(*dst, base, 2 * n);
10436 for i in 0..n {
10437 d[2 * i] = s[2 * i];
10438 d[2 * i + 1] = -s[2 * i + 1];
10439 }
10440 }
10441 }
10442
10443 Thunk::ActivationC64 {
10444 src,
10445 dst,
10446 len,
10447 kind,
10448 } => {
10449 let n = *len as usize;
10450 unsafe {
10451 let s = sl(*src, base, 2 * n);
10452 let d = sl_mut(*dst, base, 2 * n);
10453 for i in 0..n {
10454 let a = s[2 * i];
10455 let b = s[2 * i + 1];
10456 let (re, im) = match kind {
10457 Activation::Neg => (-a, -b),
10458 Activation::Exp => {
10459 let ea = a.exp();
10461 (ea * b.cos(), ea * b.sin())
10462 }
10463 Activation::Log => {
10464 let r = (a * a + b * b).sqrt();
10466 (r.ln(), b.atan2(a))
10467 }
10468 Activation::Sqrt => {
10469 let r = (a * a + b * b).sqrt();
10472 let re = ((r + a) * 0.5).max(0.0).sqrt();
10473 let im_mag = ((r - a) * 0.5).max(0.0).sqrt();
10474 let im = if b >= 0.0 { im_mag } else { -im_mag };
10475 (re, im)
10476 }
10477 _ => unreachable!("non-C64 activation kind survived lowering"),
10478 };
10479 d[2 * i] = re;
10480 d[2 * i + 1] = im;
10481 }
10482 }
10483 }
10484
10485 Thunk::Scan {
10486 body,
10487 body_init,
10488 body_input_off,
10489 body_output_off,
10490 outer_init_off,
10491 outer_final_off,
10492 length,
10493 carry_bytes,
10494 save_trajectory,
10495 xs_inputs,
10496 bcast_inputs,
10497 num_checkpoints,
10498 } => {
10499 let cb = *carry_bytes as usize;
10500 let n_steps = *length as usize;
10501 let k_total = if *num_checkpoints == 0 || *num_checkpoints == *length {
10505 n_steps } else {
10507 *num_checkpoints as usize
10508 };
10509 let checkpoint_t_for_k = |k: usize| -> usize {
10510 if k_total == n_steps {
10511 k
10512 } else {
10513 ((k + 1) * n_steps)
10514 .div_ceil(k_total)
10515 .saturating_sub(1)
10516 .min(n_steps - 1)
10517 }
10518 };
10519 let mut next_k = 0usize;
10520
10521 let mut body_buf: Vec<u8> = (**body_init).clone();
10522 unsafe {
10523 std::ptr::copy_nonoverlapping(
10524 base.add(*outer_init_off),
10525 body_buf.as_mut_ptr().add(*body_input_off),
10526 cb,
10527 );
10528 for (body_b_off, outer_b_off, total_bytes) in bcast_inputs.iter() {
10532 std::ptr::copy_nonoverlapping(
10533 base.add(*outer_b_off),
10534 body_buf.as_mut_ptr().add(*body_b_off),
10535 *total_bytes as usize,
10536 );
10537 }
10538 }
10539 for t in 0..n_steps {
10540 for (body_x_off, outer_xs_off, per_step_bytes) in xs_inputs.iter() {
10541 let psb = *per_step_bytes as usize;
10542 unsafe {
10543 std::ptr::copy_nonoverlapping(
10544 base.add(*outer_xs_off + t * psb),
10545 body_buf.as_mut_ptr().add(*body_x_off),
10546 psb,
10547 );
10548 }
10549 }
10550
10551 execute_thunks(body, &mut body_buf);
10552
10553 if *save_trajectory && next_k < k_total && t == checkpoint_t_for_k(next_k) {
10554 unsafe {
10555 std::ptr::copy_nonoverlapping(
10556 body_buf.as_ptr().add(*body_output_off),
10557 base.add(*outer_final_off + next_k * cb),
10558 cb,
10559 );
10560 }
10561 next_k += 1;
10562 }
10563
10564 if *body_output_off != *body_input_off {
10565 body_buf
10566 .copy_within(*body_output_off..*body_output_off + cb, *body_input_off);
10567 }
10568 }
10569
10570 if !*save_trajectory {
10571 unsafe {
10573 std::ptr::copy_nonoverlapping(
10574 body_buf.as_ptr().add(*body_output_off),
10575 base.add(*outer_final_off),
10576 cb,
10577 );
10578 }
10579 }
10580 }
10581
10582 Thunk::ScanBackward {
10583 body_vjp,
10584 body_init,
10585 body_carry_in_off,
10586 body_x_offs,
10587 body_d_output_off,
10588 body_dcarry_out_off,
10589 outer_init_off,
10590 outer_traj_off,
10591 outer_upstream_off,
10592 outer_xs_offs,
10593 outer_dinit_off,
10594 length,
10595 carry_bytes,
10596 save_trajectory,
10597 num_checkpoints,
10598 forward_body,
10599 forward_body_init,
10600 forward_body_carry_in_off,
10601 forward_body_output_off,
10602 forward_body_x_offs,
10603 carry_elem_size,
10604 } => {
10605 let cb = *carry_bytes as usize;
10618 let n_steps = *length as usize;
10619 let k_total = *num_checkpoints as usize;
10620 let is_recursive = k_total != 0 && k_total != n_steps;
10621 let checkpoint_t_for_k = |k: usize| -> usize {
10622 ((k + 1) * n_steps)
10623 .div_ceil(k_total)
10624 .saturating_sub(1)
10625 .min(n_steps - 1)
10626 };
10627
10628 let mut fwd_buf: Vec<u8> = if is_recursive {
10629 (**forward_body_init.as_ref().unwrap()).clone()
10630 } else {
10631 Vec::new()
10632 };
10633
10634 let mut dcarry: Vec<u8> = vec![0u8; cb];
10635 if !*save_trajectory {
10636 unsafe {
10637 std::ptr::copy_nonoverlapping(
10638 base.add(*outer_upstream_off),
10639 dcarry.as_mut_ptr(),
10640 cb,
10641 );
10642 }
10643 }
10644
10645 let mut body_buf: Vec<u8> = (**body_init).clone();
10646
10647 let process_iter =
10652 |t: usize, carry_in: &[u8], dcarry: &mut Vec<u8>, body_buf: &mut Vec<u8>| {
10653 if *save_trajectory {
10654 unsafe {
10655 let up_off = *outer_upstream_off + t * cb;
10656 match *carry_elem_size {
10657 4 => {
10658 let up_ptr = base.add(up_off) as *const f32;
10659 let dc_ptr = dcarry.as_mut_ptr() as *mut f32;
10660 let n_elems = cb / 4;
10661 for i in 0..n_elems {
10662 *dc_ptr.add(i) += *up_ptr.add(i);
10663 }
10664 }
10665 8 => {
10666 let up_ptr = base.add(up_off) as *const f64;
10667 let dc_ptr = dcarry.as_mut_ptr() as *mut f64;
10668 let n_elems = cb / 8;
10669 for i in 0..n_elems {
10670 *dc_ptr.add(i) += *up_ptr.add(i);
10671 }
10672 }
10673 other => panic!(
10674 "ScanBackward: unsupported carry elem size {other} \
10675 (only f32/f64 carries are supported today)"
10676 ),
10677 }
10678 }
10679 }
10680 body_buf[*body_carry_in_off..*body_carry_in_off + cb]
10681 .copy_from_slice(carry_in);
10682 unsafe {
10683 for (i, body_x_off) in body_x_offs.iter().enumerate() {
10684 let (outer_xs_off, per_step_bytes) = outer_xs_offs[i];
10685 let psb = per_step_bytes as usize;
10686 std::ptr::copy_nonoverlapping(
10687 base.add(outer_xs_off + t * psb),
10688 body_buf.as_mut_ptr().add(*body_x_off),
10689 psb,
10690 );
10691 }
10692 std::ptr::copy_nonoverlapping(
10693 dcarry.as_ptr(),
10694 body_buf.as_mut_ptr().add(*body_d_output_off),
10695 cb,
10696 );
10697 }
10698 execute_thunks(body_vjp, body_buf);
10699 unsafe {
10700 std::ptr::copy_nonoverlapping(
10701 body_buf.as_ptr().add(*body_dcarry_out_off),
10702 dcarry.as_mut_ptr(),
10703 cb,
10704 );
10705 }
10706 };
10707
10708 if is_recursive {
10709 let leaf_threshold = 4usize;
10717 let fb_sched = forward_body.as_ref().unwrap();
10718 let fb_init = forward_body_init.as_ref().unwrap().as_slice();
10719 let mut segment_end = n_steps - 1;
10720 for seg_k in (0..k_total).rev() {
10721 let segment_start = if seg_k == 0 {
10722 0
10723 } else {
10724 checkpoint_t_for_k(seg_k - 1) + 1
10725 };
10726 let mut anchor: Vec<u8> = vec![0u8; cb];
10727 unsafe {
10728 let src = if seg_k == 0 {
10729 base.add(*outer_init_off)
10730 } else {
10731 base.add(*outer_traj_off + (seg_k - 1) * cb)
10732 };
10733 std::ptr::copy_nonoverlapping(src, anchor.as_mut_ptr(), cb);
10734 }
10735 let mut leaf_action = |t: usize, carry_in: &[u8]| {
10738 process_iter(t, carry_in, &mut dcarry, &mut body_buf);
10739 };
10740 unsafe {
10741 griewank_process_segment(
10742 segment_start,
10743 segment_end,
10744 &anchor,
10745 cb,
10746 fb_sched,
10747 fb_init,
10748 *forward_body_carry_in_off,
10749 *forward_body_output_off,
10750 forward_body_x_offs,
10751 base,
10752 outer_xs_offs,
10753 &mut fwd_buf,
10754 leaf_threshold,
10755 &mut leaf_action,
10756 );
10757 }
10758 if seg_k == 0 {
10759 break;
10760 }
10761 segment_end = segment_start - 1;
10762 }
10763 } else {
10764 let mut carry_buf: Vec<u8> = vec![0u8; cb];
10767 for t in (0..n_steps).rev() {
10768 unsafe {
10769 let src = if t == 0 {
10770 base.add(*outer_init_off)
10771 } else {
10772 base.add(*outer_traj_off + (t - 1) * cb)
10773 };
10774 std::ptr::copy_nonoverlapping(src, carry_buf.as_mut_ptr(), cb);
10775 }
10776 process_iter(t, &carry_buf, &mut dcarry, &mut body_buf);
10777 }
10778 }
10779
10780 unsafe {
10781 std::ptr::copy_nonoverlapping(dcarry.as_ptr(), base.add(*outer_dinit_off), cb);
10782 }
10783 }
10784
10785 Thunk::ScanBackwardXs {
10786 body_vjp,
10787 body_init,
10788 body_carry_in_off,
10789 body_x_offs,
10790 body_d_output_off,
10791 body_dcarry_out_off,
10792 body_dxs_out_off,
10793 outer_init_off,
10794 outer_traj_off,
10795 outer_upstream_off,
10796 outer_xs_offs,
10797 outer_dxs_off,
10798 length,
10799 carry_bytes,
10800 carry_elem_size,
10801 per_step_bytes,
10802 save_trajectory,
10803 num_checkpoints,
10804 forward_body,
10805 forward_body_init,
10806 forward_body_carry_in_off,
10807 forward_body_output_off,
10808 forward_body_x_offs,
10809 } => {
10810 let cb = *carry_bytes as usize;
10811 let psb = *per_step_bytes as usize;
10812 let n_steps = *length as usize;
10813 let k_total = *num_checkpoints as usize;
10814 let is_recursive = k_total != 0 && k_total != n_steps;
10815 let checkpoint_t_for_k = |k: usize| -> usize {
10816 ((k + 1) * n_steps)
10817 .div_ceil(k_total)
10818 .saturating_sub(1)
10819 .min(n_steps - 1)
10820 };
10821
10822 let mut fwd_buf: Vec<u8> = if is_recursive {
10826 (**forward_body_init.as_ref().unwrap()).clone()
10827 } else {
10828 Vec::new()
10829 };
10830 let mut seg_cache: Vec<u8> = Vec::new();
10831 let mut seg_start_t: usize = usize::MAX;
10832 let mut seg_count: usize = 0;
10833 let recompute_carry_t =
10834 |t: usize,
10835 dst: &mut [u8],
10836 fwd_buf: &mut Vec<u8>,
10837 seg_cache: &mut Vec<u8>,
10838 seg_start_t: &mut usize,
10839 seg_count: &mut usize| {
10840 if !is_recursive {
10841 unsafe {
10842 let src = if t == 0 {
10843 base.add(*outer_init_off)
10844 } else {
10845 base.add(*outer_traj_off + (t - 1) * cb)
10846 };
10847 std::ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), cb);
10848 }
10849 return;
10850 }
10851 if *seg_start_t != usize::MAX
10852 && t >= *seg_start_t
10853 && t < *seg_start_t + *seg_count
10854 {
10855 let off = (t - *seg_start_t) * cb;
10856 dst.copy_from_slice(&seg_cache[off..off + cb]);
10857 return;
10858 }
10859 let seg_k = (0..k_total)
10860 .find(|&k| t <= checkpoint_t_for_k(k))
10861 .unwrap_or(k_total - 1);
10862 let (anchor_t, anchor_ptr): (usize, *const u8) = if seg_k == 0 {
10863 (0, unsafe { base.add(*outer_init_off) as *const u8 })
10864 } else {
10865 let prev_ck = checkpoint_t_for_k(seg_k - 1);
10866 (prev_ck + 1, unsafe {
10867 base.add(*outer_traj_off + (seg_k - 1) * cb) as *const u8
10868 })
10869 };
10870 let seg_end_t = checkpoint_t_for_k(seg_k);
10871 let seg_size = seg_end_t - anchor_t + 1;
10872
10873 fwd_buf.copy_from_slice(forward_body_init.as_ref().unwrap());
10874 unsafe {
10875 std::ptr::copy_nonoverlapping(
10876 anchor_ptr,
10877 fwd_buf.as_mut_ptr().add(*forward_body_carry_in_off),
10878 cb,
10879 );
10880 }
10881 seg_cache.resize(seg_size * cb, 0u8);
10882 seg_cache[0..cb].copy_from_slice(
10883 &fwd_buf[*forward_body_carry_in_off..*forward_body_carry_in_off + cb],
10884 );
10885 let fb_sched = forward_body.as_ref().unwrap();
10886 for i in 1..seg_size {
10887 let cur_iter = anchor_t + i - 1;
10888 for (idx, fb_x_off) in forward_body_x_offs.iter().enumerate() {
10889 let (outer_xs_off, x_psb) = outer_xs_offs[idx];
10890 let xb = x_psb as usize;
10891 unsafe {
10892 std::ptr::copy_nonoverlapping(
10893 base.add(outer_xs_off + cur_iter * xb),
10894 fwd_buf.as_mut_ptr().add(*fb_x_off),
10895 xb,
10896 );
10897 }
10898 }
10899 execute_thunks(fb_sched, fwd_buf);
10900 if *forward_body_output_off != *forward_body_carry_in_off {
10901 fwd_buf.copy_within(
10902 *forward_body_output_off..*forward_body_output_off + cb,
10903 *forward_body_carry_in_off,
10904 );
10905 }
10906 let cache_off = i * cb;
10907 seg_cache[cache_off..cache_off + cb].copy_from_slice(
10908 &fwd_buf
10909 [*forward_body_carry_in_off..*forward_body_carry_in_off + cb],
10910 );
10911 }
10912 *seg_start_t = anchor_t;
10913 *seg_count = seg_size;
10914
10915 let off = (t - anchor_t) * cb;
10916 dst.copy_from_slice(&seg_cache[off..off + cb]);
10917 };
10918
10919 let mut dcarry: Vec<u8> = vec![0u8; cb];
10920 if !*save_trajectory {
10921 unsafe {
10922 std::ptr::copy_nonoverlapping(
10923 base.add(*outer_upstream_off),
10924 dcarry.as_mut_ptr(),
10925 cb,
10926 );
10927 }
10928 }
10929
10930 let mut body_buf: Vec<u8> = (**body_init).clone();
10931
10932 for t in (0..n_steps).rev() {
10933 if *save_trajectory {
10934 unsafe {
10935 let up_off = *outer_upstream_off + t * cb;
10936 match *carry_elem_size {
10937 4 => {
10938 let up_ptr = base.add(up_off) as *const f32;
10939 let dc_ptr = dcarry.as_mut_ptr() as *mut f32;
10940 let n_elems = cb / 4;
10941 for i in 0..n_elems {
10942 *dc_ptr.add(i) += *up_ptr.add(i);
10943 }
10944 }
10945 8 => {
10946 let up_ptr = base.add(up_off) as *const f64;
10947 let dc_ptr = dcarry.as_mut_ptr() as *mut f64;
10948 let n_elems = cb / 8;
10949 for i in 0..n_elems {
10950 *dc_ptr.add(i) += *up_ptr.add(i);
10951 }
10952 }
10953 other => panic!(
10954 "ScanBackwardXs: unsupported carry elem size {other} \
10955 (only f32/f64 carries are supported today)"
10956 ),
10957 }
10958 }
10959 }
10960
10961 let carry_dst_start = *body_carry_in_off;
10965 {
10966 let carry_slice = &mut body_buf[carry_dst_start..carry_dst_start + cb];
10967 recompute_carry_t(
10968 t,
10969 carry_slice,
10970 &mut fwd_buf,
10971 &mut seg_cache,
10972 &mut seg_start_t,
10973 &mut seg_count,
10974 );
10975 }
10976 unsafe {
10977 for (i, body_x_off) in body_x_offs.iter().enumerate() {
10978 let (outer_xs_off, x_psb) = outer_xs_offs[i];
10979 let xb = x_psb as usize;
10980 std::ptr::copy_nonoverlapping(
10981 base.add(outer_xs_off + t * xb),
10982 body_buf.as_mut_ptr().add(*body_x_off),
10983 xb,
10984 );
10985 }
10986 std::ptr::copy_nonoverlapping(
10987 dcarry.as_ptr(),
10988 body_buf.as_mut_ptr().add(*body_d_output_off),
10989 cb,
10990 );
10991 }
10992
10993 execute_thunks(body_vjp, &mut body_buf);
10994
10995 unsafe {
10998 std::ptr::copy_nonoverlapping(
10999 body_buf.as_ptr().add(*body_dxs_out_off),
11000 base.add(*outer_dxs_off + t * psb),
11001 psb,
11002 );
11003 }
11004
11005 unsafe {
11007 std::ptr::copy_nonoverlapping(
11008 body_buf.as_ptr().add(*body_dcarry_out_off),
11009 dcarry.as_mut_ptr(),
11010 cb,
11011 );
11012 }
11013 }
11014 }
11015
11016 Thunk::FusedMmBiasAct {
11017 a,
11018 w,
11019 bias,
11020 c,
11021 m,
11022 k,
11023 n,
11024 act,
11025 } => {
11026 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
11027 unsafe {
11028 let out = sl_mut(*c, base, m * n);
11029 crate::blas::sgemm_auto(sl(*a, base, m * k), sl(*w, base, k * n), out, m, k, n);
11030 match act {
11031 Some(Activation::Gelu) => {
11032 crate::kernels::par_bias_gelu(out, sl(*bias, base, n), m, n)
11033 }
11034 Some(other) => {
11035 crate::blas::bias_add(out, sl(*bias, base, n), m, n);
11036 apply_activation_inplace(out, *other);
11037 }
11038 None => crate::blas::bias_add(out, sl(*bias, base, n), m, n),
11039 }
11040 }
11041 }
11042
11043 Thunk::FusedResidualLN {
11044 x,
11045 res,
11046 bias,
11047 g,
11048 b,
11049 out,
11050 rows,
11051 h,
11052 eps,
11053 has_bias,
11054 } => {
11055 let (rows, h) = (*rows as usize, *h as usize);
11056 unsafe {
11057 let zero = &zero_bias[..h];
11058 let bi = if *has_bias { sl(*bias, base, h) } else { zero };
11059 let x_ptr = sl(*x, base, rows * h).as_ptr() as usize;
11060 let r_ptr = sl(*res, base, rows * h).as_ptr() as usize;
11061 let o_ptr = sl_mut(*out, base, rows * h).as_mut_ptr() as usize;
11062 let bi_ptr = bi.as_ptr() as usize;
11063 let g_ptr = sl(*g, base, h).as_ptr() as usize;
11064 let b_ptr = sl(*b, base, h).as_ptr() as usize;
11065 let e = *eps;
11066 crate::pool::par_for(rows, 4, &|off, cnt| {
11067 let xs =
11068 std::slice::from_raw_parts((x_ptr as *const f32).add(off * h), cnt * h);
11069 let rs =
11070 std::slice::from_raw_parts((r_ptr as *const f32).add(off * h), cnt * h);
11071 let os = std::slice::from_raw_parts_mut(
11072 (o_ptr as *mut f32).add(off * h),
11073 cnt * h,
11074 );
11075 let bi = std::slice::from_raw_parts(bi_ptr as *const f32, h);
11076 let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
11077 let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
11078 crate::kernels::residual_bias_layer_norm(xs, rs, bi, g, b, os, cnt, h, e);
11079 });
11080 }
11081 }
11082
11083 Thunk::FusedResidualRmsNorm {
11084 x,
11085 res,
11086 bias,
11087 g,
11088 b,
11089 out,
11090 rows,
11091 h,
11092 eps,
11093 has_bias,
11094 } => {
11095 let (rows, h) = (*rows as usize, *h as usize);
11096 unsafe {
11097 let zero = &zero_bias[..h];
11098 let bi = if *has_bias { sl(*bias, base, h) } else { zero };
11099 let x_ptr = sl(*x, base, rows * h).as_ptr() as usize;
11100 let r_ptr = sl(*res, base, rows * h).as_ptr() as usize;
11101 let o_ptr = sl_mut(*out, base, rows * h).as_mut_ptr() as usize;
11102 let bi_ptr = bi.as_ptr() as usize;
11103 let g_ptr = sl(*g, base, h).as_ptr() as usize;
11104 let b_ptr = sl(*b, base, h).as_ptr() as usize;
11105 let e = *eps;
11106 crate::pool::par_for(rows, 4, &|off, cnt| {
11107 let xs =
11108 std::slice::from_raw_parts((x_ptr as *const f32).add(off * h), cnt * h);
11109 let rs =
11110 std::slice::from_raw_parts((r_ptr as *const f32).add(off * h), cnt * h);
11111 let os = std::slice::from_raw_parts_mut(
11112 (o_ptr as *mut f32).add(off * h),
11113 cnt * h,
11114 );
11115 let bi = std::slice::from_raw_parts(bi_ptr as *const f32, h);
11116 let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
11117 let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
11118 crate::kernels::residual_bias_rms_norm(xs, rs, bi, g, b, os, cnt, h, e);
11119 });
11120 }
11121 }
11122
11123 Thunk::BiasAdd {
11124 src,
11125 bias,
11126 dst,
11127 m,
11128 n,
11129 } => {
11130 let (m, n) = (*m as usize, *n as usize);
11131 let len = m * n;
11132 unsafe {
11133 let out = sl_mut(*dst, base, len);
11134 if *src != *dst {
11135 let src_ptr = base.add(*src) as *const f32;
11136 let dst_ptr = base.add(*dst) as *mut f32;
11137 if src_ptr != dst_ptr {
11138 std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, len);
11139 }
11140 }
11141 crate::blas::bias_add(out, sl(*bias, base, n), m, n);
11142 }
11143 }
11144
11145 Thunk::BinaryFull {
11146 lhs,
11147 rhs,
11148 dst,
11149 len,
11150 lhs_len,
11151 rhs_len,
11152 op,
11153 out_dims_bcast,
11154 bcast_lhs_strides,
11155 bcast_rhs_strides,
11156 elem_bytes,
11157 } => {
11158 let len = *len as usize;
11159 let ll = (*lhs_len as usize).max(1);
11160 let rl = (*rhs_len as usize).max(1);
11161 let eb = (*elem_bytes).max(1) as usize;
11162 let arena_len = arena_buf.len();
11163 let ll = ll.min((arena_len.saturating_sub(*lhs)) / eb);
11164 let rl = rl.min((arena_len.saturating_sub(*rhs)) / eb);
11165 let len = len.min((arena_len.saturating_sub(*dst)) / eb);
11166 unsafe {
11167 if eb == 8 {
11168 let l = sl_i64(*lhs, base, ll);
11169 let r = sl_i64(*rhs, base, rl);
11170 let o = sl_mut_i64(*dst, base, len);
11171 if !out_dims_bcast.is_empty() {
11172 let rank = out_dims_bcast.len();
11173 let mut coords = vec![0u32; rank];
11174 for i in 0..len {
11175 let mut rem = i;
11176 for ax in (0..rank).rev() {
11177 let sz = out_dims_bcast[ax] as usize;
11178 coords[ax] = (rem % sz) as u32;
11179 rem /= sz;
11180 }
11181 let mut li = 0usize;
11182 let mut ri = 0usize;
11183 for ax in 0..rank {
11184 li += coords[ax] as usize * bcast_lhs_strides[ax] as usize;
11185 ri += coords[ax] as usize * bcast_rhs_strides[ax] as usize;
11186 }
11187 o[i] = match op {
11188 BinaryOp::Add => l[li].wrapping_add(r[ri]),
11189 BinaryOp::Sub => l[li].wrapping_sub(r[ri]),
11190 BinaryOp::Mul => l[li].wrapping_mul(r[ri]),
11191 BinaryOp::Div => {
11192 if r[ri] == 0 {
11193 0
11194 } else {
11195 l[li] / r[ri]
11196 }
11197 }
11198 BinaryOp::Max => l[li].max(r[ri]),
11199 BinaryOp::Min => l[li].min(r[ri]),
11200 BinaryOp::Pow => l[li].pow(r[ri].max(0) as u32),
11201 };
11202 }
11203 } else {
11204 for i in 0..len {
11205 let li = if ll == 1 { 0 } else { i % ll };
11206 let ri = if rl == 1 { 0 } else { i % rl };
11207 o[i] = match op {
11208 BinaryOp::Add => l[li].wrapping_add(r[ri]),
11209 BinaryOp::Sub => l[li].wrapping_sub(r[ri]),
11210 BinaryOp::Mul => l[li].wrapping_mul(r[ri]),
11211 BinaryOp::Div => {
11212 if r[ri] == 0 {
11213 0
11214 } else {
11215 l[li] / r[ri]
11216 }
11217 }
11218 BinaryOp::Max => l[li].max(r[ri]),
11219 BinaryOp::Min => l[li].min(r[ri]),
11220 BinaryOp::Pow => l[li].pow(r[ri].max(0) as u32),
11221 };
11222 }
11223 }
11224 } else {
11225 let l = sl(*lhs, base, ll);
11226 let r = sl(*rhs, base, rl);
11227 let o = sl_mut(*dst, base, len);
11228 if ll == len && rl == len {
11229 #[cfg(target_arch = "aarch64")]
11230 if matches!(op, BinaryOp::Add | BinaryOp::Mul) {
11231 use std::arch::aarch64::*;
11232 let chunks = len / 4;
11233 for c in 0..chunks {
11234 let off = c * 4;
11235 let vl = vld1q_f32(l.as_ptr().add(off));
11236 let vr = vld1q_f32(r.as_ptr().add(off));
11237 let res = match op {
11238 BinaryOp::Add => vaddq_f32(vl, vr),
11239 BinaryOp::Mul => vmulq_f32(vl, vr),
11240 _ => unreachable!(),
11241 };
11242 vst1q_f32(o.as_mut_ptr().add(off), res);
11243 }
11244 for i in (chunks * 4)..len {
11245 o[i] = match op {
11246 BinaryOp::Add => l[i] + r[i],
11247 BinaryOp::Mul => l[i] * r[i],
11248 _ => unreachable!(),
11249 };
11250 }
11251 continue;
11252 }
11253 }
11254 if !out_dims_bcast.is_empty() {
11255 let rank = out_dims_bcast.len();
11256 let mut coords = vec![0u32; rank];
11257 for i in 0..len {
11258 let mut rem = i;
11259 for ax in (0..rank).rev() {
11260 let sz = out_dims_bcast[ax] as usize;
11261 coords[ax] = (rem % sz) as u32;
11262 rem /= sz;
11263 }
11264 let mut li = 0usize;
11265 let mut ri = 0usize;
11266 for ax in 0..rank {
11267 li += coords[ax] as usize * bcast_lhs_strides[ax] as usize;
11268 ri += coords[ax] as usize * bcast_rhs_strides[ax] as usize;
11269 }
11270 o[i] = match op {
11271 BinaryOp::Add => l[li] + r[ri],
11272 BinaryOp::Sub => l[li] - r[ri],
11273 BinaryOp::Mul => l[li] * r[ri],
11274 BinaryOp::Div => l[li] / r[ri],
11275 BinaryOp::Max => l[li].max(r[ri]),
11276 BinaryOp::Min => l[li].min(r[ri]),
11277 BinaryOp::Pow => l[li].powf(r[ri]),
11278 };
11279 }
11280 } else {
11281 for i in 0..len {
11282 let li = if ll == 1 { 0 } else { i % ll };
11283 let ri = if rl == 1 { 0 } else { i % rl };
11284 o[i] = match op {
11285 BinaryOp::Add => l[li] + r[ri],
11286 BinaryOp::Sub => l[li] - r[ri],
11287 BinaryOp::Mul => l[li] * r[ri],
11288 BinaryOp::Div => l[li] / r[ri],
11289 BinaryOp::Max => l[li].max(r[ri]),
11290 BinaryOp::Min => l[li].min(r[ri]),
11291 BinaryOp::Pow => l[li].powf(r[ri]),
11292 };
11293 }
11294 }
11295 }
11296 }
11297 }
11298
11299 Thunk::Gather {
11300 table,
11301 table_len,
11302 idx,
11303 dst,
11304 num_idx,
11305 trailing,
11306 idx_i64,
11307 table_bytes,
11308 } => {
11309 let (ni, tr) = (*num_idx as usize, *trailing as usize);
11310 let rows = *table_len as usize / tr.max(1);
11311 unsafe {
11312 if *table_bytes == 8 {
11313 let tab = sl_i64(*table, base, *table_len as usize);
11314 let out = sl_mut_i64(*dst, base, ni * tr);
11315 if *idx_i64 != 0 {
11316 let ids = sl_i64(*idx, base, ni);
11317 for i in 0..ni {
11318 let row = ids[i].max(0) as usize;
11319 if row < rows {
11320 out[i * tr..(i + 1) * tr]
11321 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
11322 }
11323 }
11324 } else {
11325 let ids = sl(*idx, base, ni);
11326 for i in 0..ni {
11327 let row = ids[i] as usize;
11328 if row < rows {
11329 out[i * tr..(i + 1) * tr]
11330 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
11331 }
11332 }
11333 }
11334 } else {
11335 let tab = sl(*table, base, *table_len as usize);
11336 let out = sl_mut(*dst, base, ni * tr);
11337 if *idx_i64 != 0 {
11338 let ids = sl_i64(*idx, base, ni);
11339 for i in 0..ni {
11340 let row = ids[i].max(0) as usize;
11341 if row < rows {
11342 out[i * tr..(i + 1) * tr]
11343 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
11344 }
11345 }
11346 } else {
11347 let ids = sl(*idx, base, ni);
11348 for i in 0..ni {
11349 let row = ids[i] as usize;
11350 if row < rows {
11351 out[i * tr..(i + 1) * tr]
11352 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
11353 }
11354 }
11355 }
11356 }
11357 }
11358 }
11359
11360 Thunk::Narrow {
11361 src,
11362 dst,
11363 outer,
11364 src_stride,
11365 dst_stride,
11366 inner,
11367 elem_bytes,
11368 } => {
11369 let (outer, ss, ds, inner, eb) = (
11370 *outer as usize,
11371 *src_stride as usize,
11372 *dst_stride as usize,
11373 *inner as usize,
11374 *elem_bytes as usize,
11375 );
11376 let row_bytes = inner.saturating_mul(eb);
11377 let src_row_stride = ss.saturating_mul(eb);
11378 let dst_row_stride = ds.saturating_mul(eb);
11379 if trace_thunks {
11380 eprintln!(
11381 "[narrow] src={} dst={} outer={outer} ss={ss} ds={ds} inner={inner} eb={eb} row={row_bytes} arena={}",
11382 *src,
11383 *dst,
11384 arena_buf.len()
11385 );
11386 }
11387 if row_bytes > 0 && *src != *dst {
11388 let arena_len = arena_buf.len();
11389 for o in 0..outer {
11390 let s_off = *src + o * src_row_stride;
11391 let d_off = *dst + o * dst_row_stride;
11392 if s_off == d_off {
11393 continue;
11394 }
11395 if s_off.saturating_add(row_bytes) > arena_len
11396 || d_off.saturating_add(row_bytes) > arena_len
11397 {
11398 break;
11399 }
11400 unsafe {
11401 std::ptr::copy_nonoverlapping(
11402 base.add(s_off),
11403 base.add(d_off),
11404 row_bytes,
11405 );
11406 }
11407 }
11408 }
11409 }
11410
11411 Thunk::Copy { src, dst, len } => {
11412 let mut len = *len as usize;
11413 if *src == *dst || len == 0 {
11414 continue;
11415 }
11416 let arena_len = arena_buf.len();
11417 let max_from_src = (arena_len.saturating_sub(*src)) / 4;
11418 let max_from_dst = (arena_len.saturating_sub(*dst)) / 4;
11419 len = len.min(max_from_src).min(max_from_dst);
11420 if len == 0 {
11421 continue;
11422 }
11423 let byte_len = len.saturating_mul(4);
11424 unsafe {
11425 std::ptr::copy(base.add(*src), base.add(*dst), byte_len);
11426 }
11427 }
11428
11429 Thunk::LayerNorm {
11430 src,
11431 g,
11432 b,
11433 dst,
11434 rows,
11435 h,
11436 eps,
11437 } => {
11438 let (rows, h) = (*rows as usize, *h as usize);
11439 unsafe {
11440 let input = sl(*src, base, rows * h);
11441 let gamma = sl(*g, base, h);
11442 let beta = sl(*b, base, h);
11443 let output = sl_mut(*dst, base, rows * h);
11444 if rows >= 4 && rows * h >= 30_000 {
11446 let i_ptr = input.as_ptr() as usize;
11447 let o_ptr = output.as_mut_ptr() as usize;
11448 let g_ptr = gamma.as_ptr() as usize;
11449 let b_ptr = beta.as_ptr() as usize;
11450 let e = *eps;
11451 crate::pool::par_for(rows, 4, &|off, cnt| {
11452 let inp = std::slice::from_raw_parts(
11453 (i_ptr as *const f32).add(off * h),
11454 cnt * h,
11455 );
11456 let out = std::slice::from_raw_parts_mut(
11457 (o_ptr as *mut f32).add(off * h),
11458 cnt * h,
11459 );
11460 let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
11461 let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
11462 for row in 0..cnt {
11463 crate::kernels::layer_norm_row(
11464 &inp[row * h..(row + 1) * h],
11465 g,
11466 b,
11467 &mut out[row * h..(row + 1) * h],
11468 h,
11469 e,
11470 );
11471 }
11472 });
11473 } else {
11474 for row in 0..rows {
11475 crate::kernels::layer_norm_row(
11476 &input[row * h..(row + 1) * h],
11477 gamma,
11478 beta,
11479 &mut output[row * h..(row + 1) * h],
11480 h,
11481 *eps,
11482 );
11483 }
11484 }
11485 }
11486 }
11487
11488 Thunk::GroupNorm {
11489 src,
11490 g,
11491 b,
11492 dst,
11493 n,
11494 c,
11495 h,
11496 w,
11497 num_groups,
11498 eps,
11499 } => {
11500 let (n, c, h, w) = (*n as usize, *c as usize, *h as usize, *w as usize);
11501 let plane = c * h * w;
11502 unsafe {
11503 let stride = plane * std::mem::size_of::<f32>();
11508 for ni in 0..n {
11509 let input = sl(*src, base.add(ni * stride), plane);
11510 let gamma = sl(*g, base, c);
11511 let beta = sl(*b, base, c);
11512 let output = sl_mut(*dst, base.add(ni * stride), plane);
11513 crate::kernels::group_norm_nchw(
11514 input,
11515 gamma,
11516 beta,
11517 output,
11518 1,
11519 c,
11520 h,
11521 w,
11522 *num_groups as usize,
11523 *eps,
11524 );
11525 }
11526 }
11527 }
11528
11529 Thunk::BatchNormInference {
11530 src,
11531 g,
11532 b,
11533 mean,
11534 var,
11535 dst,
11536 count,
11537 channels,
11538 eps,
11539 } => {
11540 let count = *count as usize;
11541 let c = *channels as usize;
11542 let n = count * c;
11543 unsafe {
11544 crate::kernels::batch_norm_inference(
11545 sl(*src, base, n),
11546 sl(*g, base, c),
11547 sl(*b, base, c),
11548 sl(*mean, base, c),
11549 sl(*var, base, c),
11550 sl_mut(*dst, base, n),
11551 c,
11552 *eps,
11553 );
11554 }
11555 }
11556
11557 Thunk::LayerNorm2d {
11558 src,
11559 g,
11560 b,
11561 dst,
11562 n,
11563 c,
11564 h,
11565 w,
11566 eps,
11567 } => {
11568 let (n, c, h, w) = (*n as usize, *c as usize, *h as usize, *w as usize);
11569 let plane = c * h * w;
11570 unsafe {
11571 let input = sl(*src, base, n * plane);
11572 let gamma = sl(*g, base, c);
11573 let beta = sl(*b, base, c);
11574 let output = sl_mut(*dst, base, n * plane);
11575 crate::kernels::layer_norm2d_nchw(input, gamma, beta, output, n, c, h, w, *eps);
11576 }
11577 }
11578
11579 Thunk::ConvTranspose2d {
11580 src,
11581 weight,
11582 dst,
11583 n,
11584 c_in,
11585 h,
11586 w_in,
11587 c_out,
11588 h_out,
11589 w_out,
11590 kh,
11591 kw,
11592 sh,
11593 sw,
11594 ph,
11595 pw,
11596 dh,
11597 dw,
11598 groups,
11599 } => {
11600 let n = *n as usize;
11601 let c_in = *c_in as usize;
11602 let h = *h as usize;
11603 let w_in = *w_in as usize;
11604 let c_out = *c_out as usize;
11605 let h_out = *h_out as usize;
11606 let w_out = *w_out as usize;
11607 unsafe {
11608 let inp = sl(*src, base, n * c_in * h * w_in);
11609 let wt = sl(
11610 *weight,
11611 base,
11612 c_in * (c_out / *groups as usize) * (*kh as usize) * (*kw as usize),
11613 );
11614 let out = sl_mut(*dst, base, n * c_out * h_out * w_out);
11615 crate::kernels::conv_transpose2d_nchw(
11616 inp,
11617 wt,
11618 out,
11619 n,
11620 c_in,
11621 h,
11622 w_in,
11623 c_out,
11624 h_out,
11625 w_out,
11626 *kh as usize,
11627 *kw as usize,
11628 *sh as usize,
11629 *sw as usize,
11630 *ph as usize,
11631 *pw as usize,
11632 *dh as usize,
11633 *dw as usize,
11634 *groups as usize,
11635 );
11636 }
11637 }
11638
11639 Thunk::ResizeNearest2x {
11640 src,
11641 dst,
11642 n,
11643 c,
11644 h,
11645 w,
11646 } => {
11647 let (n, c, h, w) = (*n as usize, *c as usize, *h as usize, *w as usize);
11648 let in_plane = c * h * w;
11649 let out_plane = c * h * 2 * w * 2;
11650 let fsz = std::mem::size_of::<f32>();
11654 unsafe {
11655 for ni in 0..n {
11656 let input = sl(*src, base.add(ni * in_plane * fsz), in_plane);
11657 let output = sl_mut(*dst, base.add(ni * out_plane * fsz), out_plane);
11658 crate::kernels::resize_nearest_2x_nchw(input, output, c, h, w);
11659 }
11660 }
11661 }
11662
11663 Thunk::AxialRope2d {
11664 src,
11665 dst,
11666 batch,
11667 seq,
11668 hidden,
11669 end_x,
11670 end_y,
11671 head_dim,
11672 num_heads,
11673 theta,
11674 repeat_factor,
11675 } => {
11676 let b = *batch as usize;
11677 let s = *seq as usize;
11678 let hdim = *head_dim as usize;
11679 let nh = *num_heads as usize;
11680 let plane = s * (*hidden as usize);
11681 let plane_bytes = plane * std::mem::size_of::<f32>();
11685 unsafe {
11686 for bi in 0..b {
11687 let input = sl(*src, base.add(bi * plane_bytes), plane);
11688 let output = sl_mut(*dst, base.add(bi * plane_bytes), plane);
11689 let rotated = rlx_ir::ops::axial_rope2d::apply_axial_rope2d(
11690 input,
11691 nh,
11692 s,
11693 hdim,
11694 *end_x as usize,
11695 *end_y as usize,
11696 *theta,
11697 *repeat_factor as usize,
11698 );
11699 output.copy_from_slice(&rotated);
11700 }
11701 }
11702 }
11703
11704 Thunk::RmsNorm {
11705 src,
11706 g,
11707 b,
11708 dst,
11709 rows,
11710 h,
11711 eps,
11712 } => {
11713 let (rows, h) = (*rows as usize, *h as usize);
11714 unsafe {
11715 let input = sl(*src, base, rows * h);
11716 let gamma = sl(*g, base, h);
11717 let beta = sl(*b, base, h);
11718 let output = sl_mut(*dst, base, rows * h);
11719 let inv_h = 1.0 / h as f32;
11720 for row in 0..rows {
11721 let in_row = &input[row * h..(row + 1) * h];
11722 let out_row = &mut output[row * h..(row + 1) * h];
11723 let mut sumsq = 0f32;
11725 for &v in in_row {
11726 sumsq += v * v;
11727 }
11728 let inv_rms = (sumsq * inv_h + *eps).sqrt().recip();
11729 for i in 0..h {
11730 out_row[i] = in_row[i] * inv_rms * gamma[i] + beta[i];
11731 }
11732 }
11733 }
11734 }
11735
11736 Thunk::Softmax { data, rows, cols } => {
11737 let (rows, cols) = (*rows as usize, *cols as usize);
11738 unsafe {
11739 crate::kernels::neon_softmax(sl_mut(*data, base, rows * cols), rows, cols);
11740 }
11741 }
11742
11743 Thunk::Cumsum {
11744 src,
11745 dst,
11746 rows,
11747 cols,
11748 exclusive,
11749 } => {
11750 let (rows, cols) = (*rows as usize, *cols as usize);
11751 unsafe {
11752 let s = sl(*src, base, rows * cols);
11753 let d = sl_mut(*dst, base, rows * cols);
11754 if *exclusive {
11755 for r in 0..rows {
11756 let mut acc = 0.0f32;
11757 for c in 0..cols {
11758 d[r * cols + c] = acc;
11759 acc += s[r * cols + c];
11760 }
11761 }
11762 } else {
11763 for r in 0..rows {
11764 let mut acc = 0.0f32;
11765 for c in 0..cols {
11766 acc += s[r * cols + c];
11767 d[r * cols + c] = acc;
11768 }
11769 }
11770 }
11771 }
11772 }
11773
11774 Thunk::Sample {
11775 logits,
11776 dst,
11777 batch,
11778 vocab,
11779 top_k,
11780 top_p,
11781 temperature,
11782 seed,
11783 } => unsafe {
11784 execute_sample_f32(
11785 *logits,
11786 *dst,
11787 *batch as usize,
11788 *vocab as usize,
11789 *top_k as usize,
11790 *top_p,
11791 *temperature,
11792 *seed,
11793 base,
11794 );
11795 },
11796
11797 Thunk::RngNormal {
11798 dst,
11799 len,
11800 mean,
11801 scale,
11802 key,
11803 op_seed,
11804 } => {
11805 let n = *len as usize;
11806 unsafe {
11807 let out = sl_mut(*dst, base, n);
11808 let opts = *schedule.rng.read().unwrap();
11809 rlx_ir::fill_normal_like(out, *mean, *scale, opts, *key, *op_seed);
11810 }
11811 }
11812
11813 Thunk::RngUniform {
11814 dst,
11815 len,
11816 low,
11817 high,
11818 key,
11819 op_seed,
11820 } => {
11821 let n = *len as usize;
11822 unsafe {
11823 let out = sl_mut(*dst, base, n);
11824 let opts = *schedule.rng.read().unwrap();
11825 rlx_ir::fill_uniform_like(out, *low, *high, opts, *key, *op_seed);
11826 }
11827 }
11828
11829 Thunk::GatedDeltaNet {
11830 q,
11831 k,
11832 v,
11833 g,
11834 beta,
11835 state,
11836 dst,
11837 batch,
11838 seq,
11839 heads,
11840 state_size,
11841 } => unsafe {
11842 execute_gated_delta_net_f32(
11843 *q,
11844 *k,
11845 *v,
11846 *g,
11847 *beta,
11848 *state,
11849 *dst,
11850 *batch as usize,
11851 *seq as usize,
11852 *heads as usize,
11853 *state_size as usize,
11854 base,
11855 );
11856 },
11857
11858 Thunk::Lstm {
11859 x,
11860 w_ih,
11861 w_hh,
11862 bias,
11863 h0,
11864 c0,
11865 dst,
11866 batch,
11867 seq,
11868 input_size,
11869 hidden,
11870 num_layers,
11871 bidirectional,
11872 carry,
11873 } => unsafe {
11874 execute_lstm_f32(
11875 *x,
11876 *w_ih,
11877 *w_hh,
11878 *bias,
11879 *h0,
11880 *c0,
11881 *dst,
11882 *batch as usize,
11883 *seq as usize,
11884 *input_size as usize,
11885 *hidden as usize,
11886 *num_layers as usize,
11887 *bidirectional,
11888 *carry,
11889 base,
11890 );
11891 },
11892
11893 Thunk::Gru {
11894 x,
11895 w_ih,
11896 w_hh,
11897 b_ih,
11898 b_hh,
11899 h0,
11900 dst,
11901 batch,
11902 seq,
11903 input_size,
11904 hidden,
11905 num_layers,
11906 bidirectional,
11907 carry,
11908 } => unsafe {
11909 execute_gru_f32(
11910 *x,
11911 *w_ih,
11912 *w_hh,
11913 *b_ih,
11914 *b_hh,
11915 *h0,
11916 *dst,
11917 *batch as usize,
11918 *seq as usize,
11919 *input_size as usize,
11920 *hidden as usize,
11921 *num_layers as usize,
11922 *bidirectional,
11923 *carry,
11924 base,
11925 );
11926 },
11927
11928 Thunk::Rnn {
11929 x,
11930 w_ih,
11931 w_hh,
11932 bias,
11933 h0,
11934 dst,
11935 batch,
11936 seq,
11937 input_size,
11938 hidden,
11939 num_layers,
11940 bidirectional,
11941 carry,
11942 relu,
11943 } => unsafe {
11944 execute_rnn_f32(
11945 *x,
11946 *w_ih,
11947 *w_hh,
11948 *bias,
11949 *h0,
11950 *dst,
11951 *batch as usize,
11952 *seq as usize,
11953 *input_size as usize,
11954 *hidden as usize,
11955 *num_layers as usize,
11956 *bidirectional,
11957 *carry,
11958 *relu,
11959 base,
11960 );
11961 },
11962
11963 Thunk::Mamba2 {
11964 x,
11965 dt,
11966 a,
11967 b,
11968 c,
11969 dst,
11970 batch,
11971 seq,
11972 heads,
11973 head_dim,
11974 state_size,
11975 } => unsafe {
11976 execute_mamba2_f32(
11977 *x,
11978 *dt,
11979 *a,
11980 *b,
11981 *c,
11982 *dst,
11983 *batch as usize,
11984 *seq as usize,
11985 *heads as usize,
11986 *head_dim as usize,
11987 *state_size as usize,
11988 base,
11989 );
11990 },
11991
11992 Thunk::SelectiveScan {
11993 x,
11994 delta,
11995 a,
11996 b: bp,
11997 c: cp,
11998 dst,
11999 batch,
12000 seq,
12001 hidden,
12002 state_size,
12003 } => unsafe {
12004 execute_selective_scan_f32(
12005 *x,
12006 *delta,
12007 *a,
12008 *bp,
12009 *cp,
12010 *dst,
12011 *batch as usize,
12012 *seq as usize,
12013 *hidden as usize,
12014 *state_size as usize,
12015 base,
12016 );
12017 },
12018
12019 Thunk::DequantMatMul {
12020 x,
12021 w_q,
12022 scale,
12023 zp,
12024 dst,
12025 m,
12026 k,
12027 n,
12028 block_size,
12029 is_asymmetric,
12030 } => {
12031 let (m, k, n, bs) = (*m as usize, *k as usize, *n as usize, *block_size as usize);
12032 let n_blocks = k.div_ceil(bs);
12033 unsafe {
12034 let xs = sl(*x, base, m * k);
12035 let w_bytes = std::slice::from_raw_parts(base.add(*w_q) as *const i8, k * n);
12036 let scales = sl(*scale, base, n_blocks * n);
12037 let zps = if *is_asymmetric {
12038 sl(*zp, base, n_blocks * n)
12039 } else {
12040 &[][..]
12041 };
12042 let out = sl_mut(*dst, base, m * n);
12043 dequant_matmul_int8(xs, w_bytes, scales, zps, out, m, k, n, bs, *is_asymmetric);
12044 }
12045 }
12046
12047 Thunk::DequantMatMulGguf {
12048 x,
12049 w_q,
12050 dst,
12051 m,
12052 k,
12053 n,
12054 scheme,
12055 } => {
12056 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
12057 let block_bytes = scheme.gguf_block_bytes() as usize;
12058 let block_elems = scheme.gguf_block_size() as usize;
12059 debug_assert!(
12060 block_bytes > 0 && block_elems > 0,
12061 "non-GGUF scheme in GGUF arm"
12062 );
12063 debug_assert!(
12064 (k * n).is_multiple_of(block_elems),
12065 "k*n={} not aligned to GGUF block size {}",
12066 k * n,
12067 block_elems
12068 );
12069 let total_bytes = (k * n) / block_elems * block_bytes;
12070 unsafe {
12071 let xs = sl(*x, base, m * k);
12072 let w_bytes_ptr = base.add(*w_q) as *const u8;
12073 let w_bytes = std::slice::from_raw_parts(w_bytes_ptr, total_bytes);
12074 let out = sl_mut(*dst, base, m * n);
12075 crate::gguf_matmul::gguf_matmul_bt(xs, w_bytes, out, m, k, n, *scheme);
12076 }
12077 }
12078
12079 Thunk::DequantMatMulInt4 {
12080 x,
12081 w_q,
12082 scale,
12083 zp,
12084 dst,
12085 m,
12086 k,
12087 n,
12088 block_size,
12089 is_asymmetric,
12090 } => {
12091 let (m, k, n, bs) = (*m as usize, *k as usize, *n as usize, *block_size as usize);
12092 let n_blocks = k.div_ceil(bs);
12093 unsafe {
12094 let xs = sl(*x, base, m * k);
12095 let w_bytes = std::slice::from_raw_parts(
12096 base.add(*w_q) as *const u8,
12097 (k * n).div_ceil(2),
12098 );
12099 let scales = sl(*scale, base, n_blocks * n);
12100 let zps = if *is_asymmetric {
12101 sl(*zp, base, n_blocks * n)
12102 } else {
12103 &[][..]
12104 };
12105 let out = sl_mut(*dst, base, m * n);
12106 dequant_matmul_int4(xs, w_bytes, scales, zps, out, m, k, n, bs, *is_asymmetric);
12107 }
12108 }
12109
12110 Thunk::DequantMatMulFp8 {
12111 x,
12112 w_q,
12113 scale,
12114 dst,
12115 m,
12116 k,
12117 n,
12118 e5m2,
12119 } => {
12120 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
12121 unsafe {
12122 let xs = sl(*x, base, m * k);
12123 let w_bytes = std::slice::from_raw_parts(base.add(*w_q) as *const u8, k * n);
12124 let scales = sl(*scale, base, n);
12125 let out = sl_mut(*dst, base, m * n);
12126 dequant_matmul_fp8(xs, w_bytes, scales, out, m, k, n, *e5m2);
12127 }
12128 }
12129
12130 Thunk::DequantMatMulNvfp4 {
12131 x,
12132 w_q,
12133 scale,
12134 global_scale,
12135 dst,
12136 m,
12137 k,
12138 n,
12139 } => {
12140 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
12141 let n_scale = k.div_ceil(rlx_ir::NVFP4_GROUP_SIZE) * n;
12142 unsafe {
12143 let xs = sl(*x, base, m * k);
12144 let w_bytes = std::slice::from_raw_parts(
12145 base.add(*w_q) as *const u8,
12146 (k * n).div_ceil(2),
12147 );
12148 let scale_bytes =
12149 std::slice::from_raw_parts(base.add(*scale) as *const u8, n_scale);
12150 let gs = sl(*global_scale, base, 1)[0];
12151 let out = sl_mut(*dst, base, m * n);
12152 dequant_matmul_nvfp4(xs, w_bytes, scale_bytes, gs, out, m, k, n);
12153 }
12154 }
12155
12156 Thunk::ScaledMatMul {
12157 lhs,
12158 rhs,
12159 lhs_scale,
12160 rhs_scale,
12161 bias,
12162 dst,
12163 m,
12164 k,
12165 n,
12166 lhs_fmt,
12167 rhs_fmt,
12168 layout,
12169 has_bias,
12170 } => {
12171 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
12172 let layout = *layout;
12173 let nblk = lowp_nblk(k, layout);
12174 let per_tensor = matches!(layout, rlx_ir::ScaleLayout::PerTensor);
12175 let n_lscale = if per_tensor { 1 } else { m * nblk };
12176 let n_rscale = if per_tensor { 1 } else { n * nblk };
12177 unsafe {
12178 let lhs_b = std::slice::from_raw_parts(base.add(*lhs) as *const u8, m * k);
12179 let rhs_b = std::slice::from_raw_parts(base.add(*rhs) as *const u8, n * k);
12180 let ls = lowp_read_scales(layout, base, *lhs_scale, n_lscale);
12181 let rs = lowp_read_scales(layout, base, *rhs_scale, n_rscale);
12182 let bias_s = if *has_bias {
12183 Some(sl(*bias, base, n))
12184 } else {
12185 None
12186 };
12187 let out = sl_mut(*dst, base, m * n);
12188 lowp_scaled_matmul(
12189 lhs_b, rhs_b, &ls, &rs, bias_s, out, m, n, k, layout, *lhs_fmt, *rhs_fmt,
12190 );
12191 }
12192 }
12193
12194 Thunk::ScaledQuantize {
12195 x,
12196 scale,
12197 dst,
12198 rows,
12199 cols,
12200 fmt,
12201 layout,
12202 } => {
12203 let (rows, cols) = (*rows as usize, *cols as usize);
12204 let layout = *layout;
12205 let nblk = lowp_nblk(cols, layout);
12206 let n_scale = if matches!(layout, rlx_ir::ScaleLayout::PerTensor) {
12207 1
12208 } else {
12209 rows * nblk
12210 };
12211 unsafe {
12212 let xs = sl(*x, base, rows * cols);
12213 let scales = lowp_read_scales(layout, base, *scale, n_scale);
12214 let out = std::slice::from_raw_parts_mut(base.add(*dst), rows * cols);
12215 lowp_quantize(xs, &scales, *fmt, layout, rows, cols, out);
12216 }
12217 }
12218
12219 Thunk::ScaledQuantScale {
12220 x,
12221 dst,
12222 rows,
12223 cols,
12224 fmt,
12225 layout,
12226 } => {
12227 let (rows, cols) = (*rows as usize, *cols as usize);
12228 let layout = *layout;
12229 let nblk = lowp_nblk(cols, layout);
12230 unsafe {
12231 let xs = sl(*x, base, rows * cols);
12232 let scales = lowp_compute_scales(xs, *fmt, layout, rows, cols);
12233 match layout {
12234 rlx_ir::ScaleLayout::PerTensor => {
12235 sl_mut(*dst, base, 1)[0] = scales[0];
12236 }
12237 rlx_ir::ScaleLayout::BlockMxE8M0 { .. } => {
12238 let out = std::slice::from_raw_parts_mut(base.add(*dst), rows * nblk);
12239 for (o, &s) in out.iter_mut().zip(&scales) {
12240 *o = rlx_ir::lowp_codec::f32_to_e8m0(s);
12241 }
12242 }
12243 rlx_ir::ScaleLayout::Nvfp4 { .. } => {
12244 let out = std::slice::from_raw_parts_mut(base.add(*dst), rows * nblk);
12245 for (o, &s) in out.iter_mut().zip(&scales) {
12246 *o = rlx_ir::lowp_codec::encode(rlx_ir::ScaledFormat::F8E4M3, s);
12247 }
12248 }
12249 }
12250 }
12251 }
12252
12253 Thunk::ScaledDequantize {
12254 codes,
12255 scale,
12256 dst,
12257 rows,
12258 cols,
12259 fmt,
12260 layout,
12261 } => unsafe {
12262 execute_scaled_dequantize_f32(
12263 *codes,
12264 *scale,
12265 *dst,
12266 *rows as usize,
12267 *cols as usize,
12268 *fmt,
12269 *layout,
12270 base,
12271 );
12272 },
12273
12274 Thunk::LoraMatMul {
12275 x,
12276 w,
12277 a,
12278 b,
12279 dst,
12280 m,
12281 k,
12282 n,
12283 r,
12284 scale,
12285 } => {
12286 let (m, k, n, r) = (*m as usize, *k as usize, *n as usize, *r as usize);
12287 unsafe {
12288 let xs = sl(*x, base, m * k);
12289 let ws = sl(*w, base, k * n);
12290 let a_s = sl(*a, base, k * r);
12291 let bs = sl(*b, base, r * n);
12292 let out = sl_mut(*dst, base, m * n);
12293 crate::blas::sgemm(xs, ws, out, m, k, n);
12294 let mut tmp = vec![0f32; m * r];
12295 crate::blas::sgemm(xs, a_s, &mut tmp, m, k, r);
12296 if *scale != 1.0 {
12297 for v in tmp.iter_mut() {
12298 *v *= *scale;
12299 }
12300 }
12301 crate::blas::sgemm_accumulate(&tmp, bs, out, m, r, n);
12302 }
12303 }
12304
12305 Thunk::Attention {
12306 q,
12307 k,
12308 v,
12309 mask,
12310 out,
12311 batch,
12312 seq,
12313 kv_seq,
12314 heads,
12315 head_dim,
12316 mask_kind,
12317 scale,
12318 q_row_stride,
12319 k_row_stride,
12320 v_row_stride,
12321 bhsd,
12322 } => {
12323 let (b, q_s, k_s, nh, dh) = (
12324 *batch as usize,
12325 *seq as usize,
12326 *kv_seq as usize,
12327 *heads as usize,
12328 *head_dim as usize,
12329 );
12330 let hs = nh * dh;
12331 let (qrs, krs, vrs) = if *bhsd {
12334 (dh, dh, dh)
12335 } else {
12336 (
12337 *q_row_stride as usize,
12338 *k_row_stride as usize,
12339 *v_row_stride as usize,
12340 )
12341 };
12342 let bhsd = *bhsd;
12343 let _ = (q_row_stride, k_row_stride, v_row_stride);
12344 let scale = *scale;
12345 let ss = q_s * k_s;
12346 let cfg = crate::config::RuntimeConfig::global();
12347 unsafe {
12348 let q_len = if bhsd {
12355 b * nh * q_s * dh
12356 } else {
12357 b * q_s * qrs
12358 };
12359 let k_len = if bhsd {
12360 b * nh * k_s * dh
12361 } else {
12362 b * k_s * krs
12363 };
12364 let v_len = if bhsd {
12365 b * nh * k_s * dh
12366 } else {
12367 b * k_s * vrs
12368 };
12369 let q_data = sl(*q, base, q_len);
12370 let k_data = sl(*k, base, k_len);
12371 let v_data = sl(*v, base, v_len);
12372 let mask_data: &[f32] = match mask_kind {
12373 rlx_ir::op::MaskKind::Custom => sl(*mask, base, b * k_s),
12374 rlx_ir::op::MaskKind::Bias => sl(*mask, base, b * nh * q_s * k_s),
12375 _ => &[],
12376 };
12377 let out_len = if bhsd {
12378 b * nh * q_s * dh
12379 } else {
12380 b * q_s * hs
12381 };
12382 let out_data = sl_mut(*out, base, out_len);
12383
12384 if bhsd {
12395 let scores = &mut sdpa_scores[..ss];
12396 for bi in 0..b {
12397 for hi in 0..nh {
12398 let q_head_base = bi * nh * q_s * dh + hi * q_s * dh;
12399 let k_head_base = bi * nh * k_s * dh + hi * k_s * dh;
12400 for qi in 0..q_s {
12402 let q_base = q_head_base + qi * dh;
12403 for ki in 0..k_s {
12404 let k_base = k_head_base + ki * dh;
12405 let mut dot = 0f32;
12406 for d in 0..dh {
12407 dot += q_data[q_base + d] * k_data[k_base + d];
12408 }
12409 scores[qi * k_s + ki] = dot * scale;
12410 if matches!(mask_kind, rlx_ir::op::MaskKind::Custom)
12411 && !mask_data.is_empty()
12412 && mask_data[bi * k_s + ki] < mask_thr
12413 {
12414 scores[qi * k_s + ki] = mask_neg;
12415 }
12416 }
12417 }
12418 if matches!(mask_kind, rlx_ir::op::MaskKind::Bias) {
12419 let off = (bi * nh + hi) * q_s * k_s;
12420 for i in 0..q_s * k_s {
12421 scores[i] += mask_data[off + i];
12422 }
12423 }
12424 apply_synthetic_mask(scores, q_s, k_s, *mask_kind);
12425 crate::kernels::neon_softmax(scores, q_s, k_s);
12426 for qi in 0..q_s {
12428 let o_base = q_head_base + qi * dh;
12429 for d in 0..dh {
12430 out_data[o_base + d] = 0.0;
12431 }
12432 for ki in 0..k_s {
12433 let sc = scores[qi * k_s + ki];
12434 if sc > score_thr {
12435 let v_base = k_head_base + ki * dh;
12436 for d in 0..dh {
12437 out_data[o_base + d] += sc * v_data[v_base + d];
12438 }
12439 }
12440 }
12441 }
12442 }
12443 }
12444 continue;
12445 }
12446
12447 if b == 1 && q_s.max(k_s) <= cfg.sdpa_seq_threshold {
12454 let scores = &mut sdpa_scores[..ss];
12456 #[cfg(target_arch = "aarch64")]
12457 let neon_chunks = dh / 4;
12458
12459 for bi in 0..b {
12460 for hi in 0..nh {
12461 for qi in 0..q_s {
12463 let q_off = bi * q_s * qrs + qi * qrs + hi * dh;
12464 for ki in 0..k_s {
12465 let k_off = bi * k_s * krs + ki * krs + hi * dh;
12466 #[cfg(target_arch = "aarch64")]
12467 let mut dot;
12468 #[cfg(not(target_arch = "aarch64"))]
12469 let mut dot = 0f32;
12470 #[cfg(target_arch = "aarch64")]
12471 {
12472 use std::arch::aarch64::*;
12473 let mut acc = vdupq_n_f32(0.0);
12474 for c in 0..neon_chunks {
12475 let vq =
12476 vld1q_f32(q_data.as_ptr().add(q_off + c * 4));
12477 let vk =
12478 vld1q_f32(k_data.as_ptr().add(k_off + c * 4));
12479 acc = vfmaq_f32(acc, vq, vk);
12480 }
12481 dot = vaddvq_f32(acc);
12482 for d in (neon_chunks * 4)..dh {
12483 dot += q_data[q_off + d] * k_data[k_off + d];
12484 }
12485 }
12486 #[cfg(not(target_arch = "aarch64"))]
12487 for d in 0..dh {
12488 dot += q_data[q_off + d] * k_data[k_off + d];
12489 }
12490 scores[qi * k_s + ki] = dot * scale;
12491 if matches!(mask_kind, rlx_ir::op::MaskKind::Custom)
12498 && !mask_data.is_empty()
12499 && mask_data[bi * k_s + ki] < mask_thr
12500 {
12501 scores[qi * k_s + ki] = mask_neg;
12502 }
12503 }
12504 }
12505
12506 if matches!(mask_kind, rlx_ir::op::MaskKind::Bias) {
12507 let off = (bi * nh + hi) * q_s * k_s;
12508 for i in 0..q_s * k_s {
12509 scores[i] += mask_data[off + i];
12510 }
12511 }
12512 apply_synthetic_mask(scores, q_s, k_s, *mask_kind);
12513 crate::kernels::neon_softmax(scores, q_s, k_s);
12514
12515 for qi in 0..q_s {
12517 let o_off = bi * q_s * hs + qi * hs + hi * dh;
12518 for d in 0..dh {
12520 out_data[o_off + d] = 0.0;
12521 }
12522 for ki in 0..k_s {
12523 let sc = scores[qi * k_s + ki];
12524 if sc > score_thr {
12525 let v_off = bi * k_s * vrs + ki * vrs + hi * dh;
12526 #[cfg(target_arch = "aarch64")]
12527 {
12528 use std::arch::aarch64::*;
12529 let vsc = vdupq_n_f32(sc);
12530 for c in 0..neon_chunks {
12531 let off = c * 4;
12532 let vo = vld1q_f32(
12533 out_data.as_ptr().add(o_off + off),
12534 );
12535 let vv =
12536 vld1q_f32(v_data.as_ptr().add(v_off + off));
12537 vst1q_f32(
12538 out_data.as_mut_ptr().add(o_off + off),
12539 vfmaq_f32(vo, vsc, vv),
12540 );
12541 }
12542 }
12543 #[cfg(not(target_arch = "aarch64"))]
12544 for d in 0..dh {
12545 out_data[o_off + d] += sc * v_data[v_off + d];
12546 }
12547 }
12548 }
12549 }
12550 }
12551 }
12552 } else {
12553 let total_work = b * nh;
12555 let q_addr = q_data.as_ptr() as usize;
12556 let k_addr = k_data.as_ptr() as usize;
12557 let v_addr = v_data.as_ptr() as usize;
12558 let m_addr = mask_data.as_ptr() as usize;
12559 let o_addr = out_data.as_mut_ptr() as usize;
12560 let sc_addr = sdpa_scores.as_mut_ptr() as usize;
12561
12562 crate::pool::par_for(total_work, 1, &|off, cnt| {
12563 for idx in off..off + cnt {
12564 let bi = idx / nh;
12565 let hi = idx % nh;
12566
12567 let q_start = (q_addr as *const f32).add(bi * q_s * qrs + hi * dh);
12568 let k_start = (k_addr as *const f32).add(bi * k_s * krs + hi * dh);
12569 let v_start = (v_addr as *const f32).add(bi * k_s * vrs + hi * dh);
12570 let o_start = (o_addr as *mut f32).add(bi * q_s * hs + hi * dh);
12571 let sc = std::slice::from_raw_parts_mut(
12572 (sc_addr as *mut f32).add(idx * ss),
12573 ss,
12574 );
12575
12576 crate::blas::sgemm_general(
12579 q_start,
12580 k_start,
12581 sc.as_mut_ptr(),
12582 q_s,
12583 k_s,
12584 dh,
12585 scale,
12586 0.0,
12587 qrs,
12588 krs,
12589 k_s,
12590 false,
12591 true,
12592 );
12593
12594 match mask_kind {
12595 rlx_ir::op::MaskKind::Custom => {
12596 let mask_bi = std::slice::from_raw_parts(
12597 (m_addr as *const f32).add(bi * k_s),
12598 k_s,
12599 );
12600 for ki in 0..k_s {
12601 if mask_bi[ki] < mask_thr {
12602 for qi in 0..q_s {
12603 sc[qi * k_s + ki] = mask_neg;
12604 }
12605 }
12606 }
12607 }
12608 rlx_ir::op::MaskKind::Bias => {
12609 let bias = std::slice::from_raw_parts(
12611 (m_addr as *const f32).add((bi * nh + hi) * q_s * k_s),
12612 q_s * k_s,
12613 );
12614 for i in 0..q_s * k_s {
12615 sc[i] += bias[i];
12616 }
12617 }
12618 _ => apply_synthetic_mask(sc, q_s, k_s, *mask_kind),
12619 }
12620
12621 crate::kernels::neon_softmax(sc, q_s, k_s);
12622
12623 crate::blas::sgemm_general(
12627 sc.as_ptr(),
12628 v_start,
12629 o_start,
12630 q_s,
12631 dh,
12632 k_s,
12633 1.0,
12634 0.0,
12635 k_s,
12636 vrs,
12637 hs,
12638 false,
12639 false,
12640 );
12641 }
12642 });
12643 }
12644 }
12645 }
12646
12647 Thunk::AttentionBackward {
12648 q,
12649 k,
12650 v,
12651 dy,
12652 mask,
12653 out,
12654 batch,
12655 seq,
12656 kv_seq,
12657 heads,
12658 head_dim,
12659 mask_kind,
12660 wrt,
12661 bhsd,
12662 } => {
12663 let (b, q_s, k_s, nh, dh) = (
12664 *batch as usize,
12665 *seq as usize,
12666 *kv_seq as usize,
12667 *heads as usize,
12668 *head_dim as usize,
12669 );
12670 unsafe {
12671 let q_len = if *bhsd {
12672 b * nh * q_s * dh
12673 } else {
12674 b * q_s * nh * dh
12675 };
12676 let k_len = if *bhsd {
12677 b * nh * k_s * dh
12678 } else {
12679 b * k_s * nh * dh
12680 };
12681 let out_len = match wrt {
12682 rlx_ir::op::AttentionBwdWrt::Key | rlx_ir::op::AttentionBwdWrt::Value => {
12683 k_len
12684 }
12685 rlx_ir::op::AttentionBwdWrt::Query => q_len,
12686 };
12687 let q_data = sl(*q, base, q_len);
12688 let k_data = sl(*k, base, k_len);
12689 let v_data = sl(*v, base, k_len);
12690 let dy_data = sl(*dy, base, q_len);
12691 let out_data = sl_mut(*out, base, out_len);
12692 let mask_data: &[f32] = if *mask != 0 {
12693 let ml = match mask_kind {
12694 rlx_ir::op::MaskKind::Custom => b * k_s,
12695 rlx_ir::op::MaskKind::Bias => b * nh * q_s * k_s,
12696 _ => 0,
12697 };
12698 sl(*mask, base, ml)
12699 } else {
12700 &[]
12701 };
12702 crate::attention_bwd::attention_backward(
12703 *wrt, q_data, k_data, v_data, dy_data, out_data, b, nh, q_s, k_s, dh,
12704 *mask_kind, mask_data, *bhsd,
12705 );
12706 }
12707 }
12708
12709 Thunk::ActivationInPlace { data, len, act } => {
12710 let len = *len as usize;
12711 unsafe {
12712 let d = sl_mut(*data, base, len);
12713 match act {
12714 Activation::Gelu => crate::kernels::par_gelu_inplace(d),
12715 Activation::GeluApprox => crate::kernels::par_gelu_approx_inplace(d),
12716 Activation::Silu => crate::kernels::par_silu_inplace(d),
12717 Activation::Relu => {
12718 for v in d.iter_mut() {
12719 *v = v.max(0.0);
12720 }
12721 }
12722 Activation::Sigmoid => {
12723 for v in d.iter_mut() {
12724 *v = 1.0 / (1.0 + (-*v).exp());
12725 }
12726 }
12727 Activation::Tanh => {
12728 for v in d.iter_mut() {
12729 *v = v.tanh();
12730 }
12731 }
12732 Activation::Exp => {
12733 for v in d.iter_mut() {
12734 *v = v.exp();
12735 }
12736 }
12737 Activation::Log => {
12738 for v in d.iter_mut() {
12739 *v = v.ln();
12740 }
12741 }
12742 Activation::Sqrt => {
12743 for v in d.iter_mut() {
12744 *v = v.sqrt();
12745 }
12746 }
12747 Activation::Rsqrt => {
12748 for v in d.iter_mut() {
12749 *v = 1.0 / v.sqrt();
12750 }
12751 }
12752 Activation::Neg => {
12753 for v in d.iter_mut() {
12754 *v = -*v;
12755 }
12756 }
12757 Activation::Abs => {
12758 for v in d.iter_mut() {
12759 *v = v.abs();
12760 }
12761 }
12762 Activation::Round => {
12763 for v in d.iter_mut() {
12764 *v = v.round();
12765 }
12766 }
12767 Activation::Sin => {
12768 for v in d.iter_mut() {
12769 *v = v.sin();
12770 }
12771 }
12772 Activation::Cos => {
12773 for v in d.iter_mut() {
12774 *v = v.cos();
12775 }
12776 }
12777 Activation::Tan => {
12778 for v in d.iter_mut() {
12779 *v = v.tan();
12780 }
12781 }
12782 Activation::Atan => {
12783 for v in d.iter_mut() {
12784 *v = v.atan();
12785 }
12786 }
12787 }
12788 }
12789 }
12790
12791 Thunk::FusedAttnBlock {
12792 hidden,
12793 qkv_w,
12794 out_w,
12795 mask,
12796 mask_kind,
12797 out,
12798 qkv_b,
12799 out_b,
12800 cos,
12801 sin,
12802 cos_len,
12803 batch,
12804 seq,
12805 hs,
12806 nh,
12807 dh,
12808 has_bias,
12809 has_rope,
12810 interleaved,
12811 } => {
12812 let (b, s) = (*batch as usize, *seq as usize);
12813 let (h, n_h, d_h) = (*hs as usize, *nh as usize, *dh as usize);
12814 let interleaved = *interleaved;
12815 let m = b * s;
12816 let scale = (d_h as f32).powf(-0.5);
12817 let half = d_h / 2;
12818 let use_custom_mask = matches!(mask_kind, rlx_ir::op::MaskKind::Custom);
12824 unsafe {
12825 let inp = sl(*hidden, base, m * h);
12826 let wq = sl(*qkv_w, base, h * 3 * h);
12827 let wo = sl(*out_w, base, h * h);
12828 let mk = if use_custom_mask {
12829 sl(*mask, base, b * s)
12830 } else {
12831 &[]
12832 };
12833 let dst = sl_mut(*out, base, m * h);
12834
12835 let mut qkv = vec![0f32; m * 3 * h];
12837 let mut attn_out = vec![0f32; m * h];
12838 let mut scores_buf = vec![0f32; s * s]; crate::blas::sgemm(inp, wq, &mut qkv, m, h, 3 * h);
12842 if *has_bias {
12843 let bias = sl(*qkv_b, base, 3 * h);
12844 crate::blas::bias_add(&mut qkv, bias, m, 3 * h);
12845 }
12846
12847 #[cfg(target_arch = "aarch64")]
12850 let neon_chunks = d_h / 4;
12851 #[cfg(target_arch = "aarch64")]
12852 let _rope_chunks = half / 4;
12853
12854 for bi in 0..b {
12855 for hi in 0..n_h {
12856 for qi in 0..s {
12858 let q_base = bi * s * 3 * h + qi * 3 * h + hi * d_h;
12859 for ki in 0..s {
12860 let k_base = bi * s * 3 * h + ki * 3 * h + h + hi * d_h;
12861 let mut dot = 0f32;
12862
12863 if *has_rope {
12864 let q_cos = qi * half;
12866 let k_cos = ki * half;
12867 let cos_tab = sl(*cos, base, *cos_len as usize);
12868 let sin_tab = sl(*sin, base, *cos_len as usize);
12869 for i in 0..half {
12875 let (qo1, qo2, ko1, ko2) = if interleaved {
12876 (2 * i, 2 * i + 1, 2 * i, 2 * i + 1)
12877 } else {
12878 (i, half + i, i, half + i)
12879 };
12880 let q1 = qkv[q_base + qo1];
12881 let q2 = qkv[q_base + qo2];
12882 let k1 = qkv[k_base + ko1];
12883 let k2 = qkv[k_base + ko2];
12884 let c_q = cos_tab[q_cos + i];
12885 let s_q = sin_tab[q_cos + i];
12886 let c_k = cos_tab[k_cos + i];
12887 let s_k = sin_tab[k_cos + i];
12888 let qr1 = q1 * c_q - q2 * s_q;
12889 let kr1 = k1 * c_k - k2 * s_k;
12890 let qr2 = q2 * c_q + q1 * s_q;
12891 let kr2 = k2 * c_k + k1 * s_k;
12892 dot += qr1 * kr1 + qr2 * kr2;
12893 }
12894 } else {
12895 #[cfg(target_arch = "aarch64")]
12897 {
12898 use std::arch::aarch64::*;
12899 let mut acc = vdupq_n_f32(0.0);
12900 for c in 0..neon_chunks {
12901 let vq =
12902 vld1q_f32(qkv.as_ptr().add(q_base + c * 4));
12903 let vk =
12904 vld1q_f32(qkv.as_ptr().add(k_base + c * 4));
12905 acc = vfmaq_f32(acc, vq, vk);
12906 }
12907 dot = vaddvq_f32(acc);
12908 for d in (neon_chunks * 4)..d_h {
12909 dot += qkv[q_base + d] * qkv[k_base + d];
12910 }
12911 }
12912 #[cfg(not(target_arch = "aarch64"))]
12913 for d in 0..d_h {
12914 dot += qkv[q_base + d] * qkv[k_base + d];
12915 }
12916 }
12917
12918 scores_buf[qi * s + ki] = dot * scale;
12919 let pos_masked = match mask_kind {
12923 rlx_ir::op::MaskKind::Causal => ki > qi,
12924 rlx_ir::op::MaskKind::SlidingWindow(w) => {
12925 ki > qi || ki + *w < qi
12926 }
12927 _ => false,
12928 };
12929 if pos_masked || (use_custom_mask && mk[bi * s + ki] < mask_thr)
12930 {
12931 scores_buf[qi * s + ki] = mask_neg;
12932 }
12933 }
12934 }
12935
12936 crate::kernels::neon_softmax(&mut scores_buf[..s * s], s, s);
12938
12939 for qi in 0..s {
12941 let o_base = bi * s * h + qi * h + hi * d_h;
12942 for d in 0..d_h {
12943 attn_out[o_base + d] = 0.0;
12944 }
12945 for ki in 0..s {
12946 let sc = scores_buf[qi * s + ki];
12947 if sc > score_thr {
12948 let v_base = bi * s * 3 * h + ki * 3 * h + 2 * h + hi * d_h;
12949 #[cfg(target_arch = "aarch64")]
12950 {
12951 use std::arch::aarch64::*;
12952 let vsc = vdupq_n_f32(sc);
12953 for c in 0..neon_chunks {
12954 let off = c * 4;
12955 let vo =
12956 vld1q_f32(attn_out.as_ptr().add(o_base + off));
12957 let vv = vld1q_f32(qkv.as_ptr().add(v_base + off));
12958 vst1q_f32(
12959 attn_out.as_mut_ptr().add(o_base + off),
12960 vfmaq_f32(vo, vsc, vv),
12961 );
12962 }
12963 }
12964 #[cfg(not(target_arch = "aarch64"))]
12965 for d in 0..d_h {
12966 attn_out[o_base + d] += sc * qkv[v_base + d];
12967 }
12968 }
12969 }
12970 }
12971 }
12972 }
12973
12974 crate::blas::sgemm(&attn_out, wo, dst, m, h, h);
12976 if *has_bias {
12977 let bias = sl(*out_b, base, h);
12978 crate::blas::bias_add(dst, bias, m, h);
12979 }
12980 }
12981 }
12982
12983 Thunk::Rope {
12984 src,
12985 cos,
12986 sin,
12987 dst,
12988 batch,
12989 seq,
12990 hidden,
12991 head_dim,
12992 n_rot,
12993 cos_len,
12994 src_row_stride,
12995 interleaved,
12996 } => {
12997 let interleaved = *interleaved;
12998 let (b, s, hs, dh, nr) = (
12999 *batch as usize,
13000 *seq as usize,
13001 *hidden as usize,
13002 *head_dim as usize,
13003 *n_rot as usize,
13004 );
13005 let tab_half = dh / 2;
13006 let rot_half = nr / 2;
13007 let nh = hs / dh;
13008 let cl = *cos_len as usize;
13009 let src_rs = *src_row_stride as usize;
13010 let cos_rows = cl / tab_half.max(1);
13015 let per_token = cos_rows == b * s && cos_rows != s;
13016 unsafe {
13017 let x = sl(*src, base, b * s * src_rs);
13018 let cos_tab = sl(*cos, base, cl);
13019 let sin_tab = sl(*sin, base, cl);
13020 let out = sl_mut(*dst, base, b * s * hs);
13021
13022 let total = b * s;
13023 let x_ptr = x.as_ptr() as usize;
13024 let o_ptr = out.as_mut_ptr() as usize;
13025 let c_ptr = cos_tab.as_ptr() as usize;
13026 let s_ptr = sin_tab.as_ptr() as usize;
13027
13028 crate::pool::par_for(total, 4, &|off, cnt| {
13029 for idx in off..off + cnt {
13030 let bi = idx / s;
13031 let si = idx % s;
13032 let tab_off = if per_token { idx } else { si } * tab_half;
13033
13034 for hi in 0..nh {
13035 let src_base = bi * s * src_rs + si * src_rs + hi * dh;
13036 let dst_base = bi * s * hs + si * hs + hi * dh;
13037 let xp = (x_ptr as *const f32).add(src_base);
13038 let op = (o_ptr as *mut f32).add(dst_base);
13039 let cp = (c_ptr as *const f32).add(tab_off);
13040 let sp = (s_ptr as *const f32).add(tab_off);
13041
13042 if interleaved {
13043 for i in 0..rot_half {
13046 let x1 = *xp.add(2 * i);
13047 let x2 = *xp.add(2 * i + 1);
13048 let cv = *cp.add(i);
13049 let sv = *sp.add(i);
13050 *op.add(2 * i) = x1 * cv - x2 * sv;
13051 *op.add(2 * i + 1) = x2 * cv + x1 * sv;
13052 }
13053 } else {
13054 for i in 0..rot_half {
13056 let x1 = *xp.add(i);
13057 let x2 = *xp.add(rot_half + i);
13058 let cv = *cp.add(i);
13059 let sv = *sp.add(i);
13060 *op.add(i) = x1 * cv - x2 * sv;
13061 *op.add(rot_half + i) = x2 * cv + x1 * sv;
13062 }
13063 }
13064 for j in nr..dh {
13065 *op.add(j) = *xp.add(j);
13066 }
13067 }
13068 }
13069 });
13070 }
13071 }
13072 Thunk::FusedBertLayer {
13073 hidden,
13074 qkv_w,
13075 qkv_b,
13076 out_w,
13077 out_b,
13078 mask,
13079 ln1_g,
13080 ln1_b,
13081 eps1,
13082 fc1_w,
13083 fc1_b,
13084 fc2_w,
13085 fc2_b,
13086 ln2_g,
13087 ln2_b,
13088 eps2,
13089 out,
13090 batch,
13091 seq,
13092 hs,
13093 nh,
13094 dh,
13095 int_dim,
13096 } => {
13097 let (b, s, h, n_h, d_h) = (
13098 *batch as usize,
13099 *seq as usize,
13100 *hs as usize,
13101 *nh as usize,
13102 *dh as usize,
13103 );
13104 let m = b * s;
13105 let id = *int_dim as usize;
13106 let scale = (d_h as f32).powf(-0.5);
13107 let _half = d_h / 2;
13108 #[cfg(target_arch = "aarch64")]
13109 let neon_chunks = d_h / 4;
13110 unsafe {
13111 let inp = sl(*hidden, base, m * h);
13112 let dst = sl_mut(*out, base, m * h);
13113 let mk = sl(*mask, base, b * s);
13114
13115 let qkv = std::slice::from_raw_parts_mut(fl_qkv.as_mut_ptr(), m * 3 * h);
13117 let attn = std::slice::from_raw_parts_mut(fl_attn.as_mut_ptr(), m * h);
13118 let res = std::slice::from_raw_parts_mut(fl_res.as_mut_ptr(), m * h);
13119 let normed = std::slice::from_raw_parts_mut(fl_normed.as_mut_ptr(), m * h);
13120 let ffn = std::slice::from_raw_parts_mut(fl_ffn.as_mut_ptr(), m * id);
13121 let sc = std::slice::from_raw_parts_mut(fl_sc.as_mut_ptr(), s * s);
13122
13123 crate::blas::par_sgemm_bias(
13125 inp,
13126 sl(*qkv_w, base, h * 3 * h),
13127 sl(*qkv_b, base, 3 * h),
13128 qkv,
13129 m,
13130 h,
13131 3 * h,
13132 );
13133
13134 for bi in 0..b {
13136 for hi in 0..n_h {
13137 for qi in 0..s {
13138 for ki in 0..s {
13139 let q_base = bi * s * 3 * h + qi * 3 * h + hi * d_h;
13140 let k_base = bi * s * 3 * h + ki * 3 * h + h + hi * d_h;
13141 #[cfg(target_arch = "aarch64")]
13142 let dot;
13143 #[cfg(not(target_arch = "aarch64"))]
13144 let mut dot = 0f32;
13145 #[cfg(target_arch = "aarch64")]
13146 {
13147 use std::arch::aarch64::*;
13148 let mut acc = vdupq_n_f32(0.0);
13149 for c in 0..neon_chunks {
13150 acc = vfmaq_f32(
13151 acc,
13152 vld1q_f32(qkv.as_ptr().add(q_base + c * 4)),
13153 vld1q_f32(qkv.as_ptr().add(k_base + c * 4)),
13154 );
13155 }
13156 dot = vaddvq_f32(acc);
13157 }
13158 #[cfg(not(target_arch = "aarch64"))]
13159 for d in 0..d_h {
13160 dot += qkv[q_base + d] * qkv[k_base + d];
13161 }
13162 sc[qi * s + ki] = dot * scale;
13163 if mk[bi * s + ki] < mask_thr {
13164 sc[qi * s + ki] = mask_neg;
13165 }
13166 }
13167 }
13168 crate::kernels::neon_softmax(&mut sc[..s * s], s, s);
13169 for qi in 0..s {
13170 let o = bi * s * h + qi * h + hi * d_h;
13171 for d in 0..d_h {
13172 attn[o + d] = 0.0;
13173 }
13174 for ki in 0..s {
13175 let w = sc[qi * s + ki];
13176 if w > score_thr {
13177 let v = bi * s * 3 * h + ki * 3 * h + 2 * h + hi * d_h;
13178 #[cfg(target_arch = "aarch64")]
13179 {
13180 use std::arch::aarch64::*;
13181 let vw = vdupq_n_f32(w);
13182 for c in 0..neon_chunks {
13183 let off = c * 4;
13184 vst1q_f32(
13185 attn.as_mut_ptr().add(o + off),
13186 vfmaq_f32(
13187 vld1q_f32(attn.as_ptr().add(o + off)),
13188 vw,
13189 vld1q_f32(qkv.as_ptr().add(v + off)),
13190 ),
13191 );
13192 }
13193 }
13194 #[cfg(not(target_arch = "aarch64"))]
13195 for d in 0..d_h {
13196 attn[o + d] += w * qkv[v + d];
13197 }
13198 }
13199 }
13200 }
13201 }
13202 }
13203
13204 crate::blas::sgemm_bias(
13206 attn,
13207 sl(*out_w, base, h * h),
13208 sl(*out_b, base, h),
13209 res,
13210 m,
13211 h,
13212 h,
13213 );
13214 #[cfg(target_arch = "aarch64")]
13215 {
13216 use std::arch::aarch64::*;
13217 let chunks_h = (m * h) / 4;
13218 for c in 0..chunks_h {
13219 let off = c * 4;
13220 vst1q_f32(
13221 res.as_mut_ptr().add(off),
13222 vaddq_f32(
13223 vld1q_f32(res.as_ptr().add(off)),
13224 vld1q_f32(inp.as_ptr().add(off)),
13225 ),
13226 );
13227 }
13228 for i in (chunks_h * 4)..(m * h) {
13229 res[i] += inp[i];
13230 }
13231 }
13232 #[cfg(not(target_arch = "aarch64"))]
13233 for i in 0..m * h {
13234 res[i] += inp[i];
13235 }
13236
13237 let g1 = sl(*ln1_g, base, h);
13239 let b1 = sl(*ln1_b, base, h);
13240 for r in 0..m {
13241 crate::kernels::layer_norm_row(
13242 &res[r * h..(r + 1) * h],
13243 g1,
13244 b1,
13245 &mut normed[r * h..(r + 1) * h],
13246 h,
13247 *eps1,
13248 );
13249 }
13250
13251 crate::blas::par_sgemm_bias(
13253 normed,
13254 sl(*fc1_w, base, h * id),
13255 sl(*fc1_b, base, id),
13256 ffn,
13257 m,
13258 h,
13259 id,
13260 );
13261 crate::kernels::par_gelu_inplace(ffn);
13262
13263 crate::blas::par_sgemm_bias(
13265 ffn,
13266 sl(*fc2_w, base, id * h),
13267 sl(*fc2_b, base, h),
13268 res,
13269 m,
13270 id,
13271 h,
13272 );
13273 #[cfg(target_arch = "aarch64")]
13274 {
13275 use std::arch::aarch64::*;
13276 let chunks_h = (m * h) / 4;
13277 for c in 0..chunks_h {
13278 let off = c * 4;
13279 vst1q_f32(
13280 res.as_mut_ptr().add(off),
13281 vaddq_f32(
13282 vld1q_f32(res.as_ptr().add(off)),
13283 vld1q_f32(normed.as_ptr().add(off)),
13284 ),
13285 );
13286 }
13287 for i in (chunks_h * 4)..(m * h) {
13288 res[i] += normed[i];
13289 }
13290 }
13291 #[cfg(not(target_arch = "aarch64"))]
13292 for i in 0..m * h {
13293 res[i] += normed[i];
13294 }
13295
13296 let g2 = sl(*ln2_g, base, h);
13298 let b2 = sl(*ln2_b, base, h);
13299 for r in 0..m {
13300 crate::kernels::layer_norm_row(
13301 &res[r * h..(r + 1) * h],
13302 g2,
13303 b2,
13304 &mut dst[r * h..(r + 1) * h],
13305 h,
13306 *eps2,
13307 );
13308 }
13309 }
13310 }
13311
13312 Thunk::FusedNomicLayer {
13313 hidden,
13314 qkv_w,
13315 out_w,
13316 mask,
13317 cos,
13318 sin,
13319 cos_len,
13320 ln1_g,
13321 ln1_b,
13322 eps1,
13323 fc11_w,
13324 fc12_w: _,
13325 fc2_w,
13326 ln2_g,
13327 ln2_b,
13328 eps2,
13329 out,
13330 batch,
13331 seq,
13332 hs,
13333 nh,
13334 dh,
13335 int_dim,
13336 interleaved,
13337 } => {
13338 let interleaved = *interleaved;
13339 let (b, s, h, n_h, d_h) = (
13340 *batch as usize,
13341 *seq as usize,
13342 *hs as usize,
13343 *nh as usize,
13344 *dh as usize,
13345 );
13346 let m = b * s;
13347 let id = *int_dim as usize;
13348 let scale = (d_h as f32).powf(-0.5);
13349 let half_dh = d_h / 2;
13350 #[cfg(target_arch = "aarch64")]
13351 let neon_chunks = d_h / 4;
13352 unsafe {
13353 let inp = sl(*hidden, base, m * h);
13354 let dst = sl_mut(*out, base, m * h);
13355 let mk = sl(*mask, base, b * s);
13356 let cos_tab = sl(*cos, base, *cos_len as usize);
13357 let sin_tab = sl(*sin, base, *cos_len as usize);
13358 let fused_fc_w = sl(*fc11_w, base, h * 2 * id);
13360
13361 let mut qkv = vec![0f32; m * 3 * h];
13362 let mut attn = vec![0f32; m * h];
13363 let mut res = vec![0f32; m * h];
13364 let mut normed = vec![0f32; m * h];
13365 let mut ffn_concat = vec![0f32; m * 2 * id]; let mut sc = vec![0f32; s * s];
13367
13368 crate::blas::sgemm(inp, sl(*qkv_w, base, h * 3 * h), &mut qkv, m, h, 3 * h);
13370
13371 for bi in 0..b {
13373 for hi in 0..n_h {
13374 for qi in 0..s {
13375 for ki in 0..s {
13376 let q_base = bi * s * 3 * h + qi * 3 * h + hi * d_h;
13377 let k_base = bi * s * 3 * h + ki * 3 * h + h + hi * d_h;
13378 let mut dot = 0f32;
13379 for i in 0..half_dh {
13380 let (o1, o2) = if interleaved {
13382 (2 * i, 2 * i + 1)
13383 } else {
13384 (i, half_dh + i)
13385 };
13386 let q1 = qkv[q_base + o1];
13387 let q2 = qkv[q_base + o2];
13388 let k1 = qkv[k_base + o1];
13389 let k2 = qkv[k_base + o2];
13390 let cq = cos_tab[qi * half_dh + i];
13391 let sq = sin_tab[qi * half_dh + i];
13392 let ck = cos_tab[ki * half_dh + i];
13393 let sk = sin_tab[ki * half_dh + i];
13394 dot += (q1 * cq - q2 * sq) * (k1 * ck - k2 * sk)
13395 + (q2 * cq + q1 * sq) * (k2 * ck + k1 * sk);
13396 }
13397 sc[qi * s + ki] = dot * scale;
13398 if mk[bi * s + ki] < mask_thr {
13399 sc[qi * s + ki] = mask_neg;
13400 }
13401 }
13402 }
13403 crate::kernels::neon_softmax(&mut sc[..s * s], s, s);
13404 for qi in 0..s {
13405 let o = bi * s * h + qi * h + hi * d_h;
13406 for d in 0..d_h {
13407 attn[o + d] = 0.0;
13408 }
13409 for ki in 0..s {
13410 let w = sc[qi * s + ki];
13411 if w > score_thr {
13412 let v = bi * s * 3 * h + ki * 3 * h + 2 * h + hi * d_h;
13413 #[cfg(target_arch = "aarch64")]
13414 {
13415 use std::arch::aarch64::*;
13416 let vw = vdupq_n_f32(w);
13417 for c in 0..neon_chunks {
13418 let off = c * 4;
13419 vst1q_f32(
13420 attn.as_mut_ptr().add(o + off),
13421 vfmaq_f32(
13422 vld1q_f32(attn.as_ptr().add(o + off)),
13423 vw,
13424 vld1q_f32(qkv.as_ptr().add(v + off)),
13425 ),
13426 );
13427 }
13428 }
13429 #[cfg(not(target_arch = "aarch64"))]
13430 for d in 0..d_h {
13431 attn[o + d] += w * qkv[v + d];
13432 }
13433 }
13434 }
13435 }
13436 }
13437 }
13438
13439 crate::blas::sgemm(&attn, sl(*out_w, base, h * h), &mut res, m, h, h);
13441 for i in 0..m * h {
13442 res[i] += inp[i];
13443 }
13444
13445 let g1 = sl(*ln1_g, base, h);
13447 let b1 = sl(*ln1_b, base, h);
13448 for r in 0..m {
13449 crate::kernels::layer_norm_row(
13450 &res[r * h..(r + 1) * h],
13451 g1,
13452 b1,
13453 &mut normed[r * h..(r + 1) * h],
13454 h,
13455 *eps1,
13456 );
13457 }
13458
13459 crate::blas::sgemm(&normed, fused_fc_w, &mut ffn_concat, m, h, 2 * id);
13461 for row in 0..m {
13464 let bo = row * 2 * id;
13465 for j in 0..id {
13467 let x = ffn_concat[bo + id + j];
13468 ffn_concat[bo + id + j] = x / (1.0 + (-x).exp());
13469 }
13470 for j in 0..id {
13472 ffn_concat[bo + j] *= ffn_concat[bo + id + j];
13473 }
13474 }
13475
13476 let mut swiglu_contig = vec![0f32; m * id];
13482 for row in 0..m {
13483 let bo = row * 2 * id;
13484 swiglu_contig[row * id..(row + 1) * id]
13485 .copy_from_slice(&ffn_concat[bo..bo + id]);
13486 }
13487 crate::blas::sgemm(
13488 &swiglu_contig,
13489 sl(*fc2_w, base, id * h),
13490 &mut res,
13491 m,
13492 id,
13493 h,
13494 );
13495 for i in 0..m * h {
13496 res[i] += normed[i];
13497 }
13498
13499 let g2 = sl(*ln2_g, base, h);
13501 let b2 = sl(*ln2_b, base, h);
13502 for r in 0..m {
13503 crate::kernels::layer_norm_row(
13504 &res[r * h..(r + 1) * h],
13505 g2,
13506 b2,
13507 &mut dst[r * h..(r + 1) * h],
13508 h,
13509 *eps2,
13510 );
13511 }
13512 }
13513 }
13514
13515 Thunk::FusedSwiGLU {
13516 src,
13517 dst,
13518 n_half,
13519 total,
13520 gate_first,
13521 } => {
13522 let n = *n_half as usize;
13523 let t = *total as usize;
13524 let outer = t / n;
13525 let in_total = outer * 2 * n;
13526 let gate_first = *gate_first;
13527 unsafe {
13528 let inp = sl(*src, base, in_total);
13529 let out = sl_mut(*dst, base, t);
13530 for o in 0..outer {
13531 let in_row = &inp[o * 2 * n..(o + 1) * 2 * n];
13532 let out_row = &mut out[o * n..(o + 1) * n];
13533 for i in 0..n {
13534 let (up, gate) = if gate_first {
13535 (in_row[n + i], in_row[i])
13536 } else {
13537 (in_row[i], in_row[n + i])
13538 };
13539 out_row[i] = up * (gate / (1.0 + (-gate).exp()));
13540 }
13541 }
13542 }
13543 }
13544
13545 Thunk::Concat {
13546 dst,
13547 outer,
13548 inner,
13549 total_axis,
13550 inputs,
13551 } => {
13552 let outer = *outer as usize;
13553 let inner = *inner as usize;
13554 let total_axis = *total_axis as usize;
13555 let row_stride = total_axis * inner;
13556 let out_total = outer * row_stride;
13557 unsafe {
13558 let out = sl_mut(*dst, base, out_total);
13559 let mut cum: usize = 0;
13560 for (src_off, in_axis, in_numel) in inputs {
13561 let in_axis = *in_axis as usize;
13562 let copy_per_row = in_axis * inner;
13563 let dst_col_off = cum * inner;
13564 let inp = sl(*src_off, base, (*in_numel as usize).max(1));
13565 concat_copy_rows_f32(
13566 out,
13567 inp,
13568 outer,
13569 copy_per_row,
13570 row_stride,
13571 dst_col_off,
13572 *in_numel as usize,
13573 );
13574 cum += in_axis;
13575 }
13576 }
13577 }
13578
13579 Thunk::ConcatF64 {
13580 dst,
13581 outer,
13582 inner,
13583 total_axis,
13584 inputs,
13585 } => {
13586 let outer = *outer as usize;
13587 let inner = *inner as usize;
13588 let total_axis = *total_axis as usize;
13589 let row_stride = total_axis * inner;
13590 let out_total = outer * row_stride;
13591 unsafe {
13592 let out = sl_mut_f64(*dst, base, out_total);
13593 let mut cum: usize = 0;
13594 for (src_off, in_axis, in_numel) in inputs {
13595 let in_axis = *in_axis as usize;
13596 let copy_per_row = in_axis * inner;
13597 let dst_col_off = cum * inner;
13598 let inp = sl_f64(*src_off, base, (*in_numel as usize).max(1));
13599 concat_copy_rows_f64(
13600 out,
13601 inp,
13602 outer,
13603 copy_per_row,
13604 row_stride,
13605 dst_col_off,
13606 *in_numel as usize,
13607 );
13608 cum += in_axis;
13609 }
13610 }
13611 }
13612
13613 Thunk::Compare {
13614 lhs,
13615 rhs,
13616 dst,
13617 len,
13618 op,
13619 inputs_i64,
13620 inputs_elem_bytes,
13621 dst_elem_bytes,
13622 } => {
13623 let len = *len as usize;
13624 let arena_len = arena_buf.len();
13625 let elem = (*inputs_elem_bytes).max(1) as usize;
13626 let dst_eb = (*dst_elem_bytes).max(1) as usize;
13627 let max_l = (arena_len.saturating_sub(*lhs)) / elem;
13628 let max_r = (arena_len.saturating_sub(*rhs)) / elem;
13629 let max_d = (arena_len.saturating_sub(*dst)) / dst_eb;
13630 let len = len.min(max_l).min(max_r).min(max_d);
13631 if trace_thunks && len > 0 {
13632 eprintln!("[compare] len={len} lhs={} rhs={} dst={}", *lhs, *rhs, *dst);
13633 }
13634 if elem == 1 {
13635 let l = arena_buf[*lhs..*lhs + len].to_vec();
13636 let r = arena_buf[*rhs..*rhs + len].to_vec();
13637 for i in 0..len {
13638 let v = match op {
13639 CmpOp::Eq => l[i] == r[i],
13640 CmpOp::Ne => l[i] != r[i],
13641 CmpOp::Lt => l[i] < r[i],
13642 CmpOp::Le => l[i] <= r[i],
13643 CmpOp::Gt => l[i] > r[i],
13644 CmpOp::Ge => l[i] >= r[i],
13645 };
13646 if *dst_elem_bytes == 1 {
13647 arena_buf[*dst + i] = u8::from(v);
13648 } else {
13649 unsafe {
13650 let o = sl_mut(*dst, base, len);
13651 o[i] = if v { 1.0 } else { 0.0 };
13652 }
13653 }
13654 }
13655 } else if *inputs_i64 != 0 {
13656 unsafe {
13657 let l = sl_i64(*lhs, base, len);
13658 let r = sl_i64(*rhs, base, len);
13659 for i in 0..len {
13660 let v = match op {
13661 CmpOp::Eq => l[i] == r[i],
13662 CmpOp::Ne => l[i] != r[i],
13663 CmpOp::Lt => l[i] < r[i],
13664 CmpOp::Le => l[i] <= r[i],
13665 CmpOp::Gt => l[i] > r[i],
13666 CmpOp::Ge => l[i] >= r[i],
13667 };
13668 if *dst_elem_bytes == 1 {
13669 arena_buf[*dst + i] = u8::from(v);
13670 } else {
13671 let o = sl_mut(*dst, base, len);
13672 o[i] = if v { 1.0 } else { 0.0 };
13673 }
13674 }
13675 }
13676 } else {
13677 unsafe {
13678 let l = sl(*lhs, base, len);
13679 let r = sl(*rhs, base, len);
13680 for i in 0..len {
13681 let v = match op {
13682 CmpOp::Eq => l[i] == r[i],
13683 CmpOp::Ne => l[i] != r[i],
13684 CmpOp::Lt => l[i] < r[i],
13685 CmpOp::Le => l[i] <= r[i],
13686 CmpOp::Gt => l[i] > r[i],
13687 CmpOp::Ge => l[i] >= r[i],
13688 };
13689 if *dst_elem_bytes == 1 {
13690 arena_buf[*dst + i] = u8::from(v);
13691 } else {
13692 let o = sl_mut(*dst, base, len);
13693 o[i] = if v { 1.0 } else { 0.0 };
13694 }
13695 }
13696 }
13697 }
13698 }
13699
13700 Thunk::Where {
13701 cond,
13702 on_true,
13703 on_false,
13704 dst,
13705 len,
13706 elem_bytes,
13707 cond_elem_bytes,
13708 } => {
13709 let len = *len as usize;
13710 let eb = *elem_bytes as usize;
13711 let cond_eb = (*cond_elem_bytes).max(1) as usize;
13712 let arena_len = arena_buf.len();
13713 let len = len
13714 .min((arena_len.saturating_sub(*cond)) / cond_eb)
13715 .min((arena_len.saturating_sub(*on_true)) / eb)
13716 .min((arena_len.saturating_sub(*on_false)) / eb)
13717 .min((arena_len.saturating_sub(*dst)) / eb);
13718 unsafe {
13719 if *elem_bytes == 8 {
13720 let t = sl_i64(*on_true, base, len);
13721 let e = sl_i64(*on_false, base, len);
13722 let o = sl_mut_i64(*dst, base, len);
13723 if *cond_elem_bytes == 1 {
13724 let c = &arena_buf[*cond..*cond + len];
13725 for i in 0..len {
13726 o[i] = if c[i] != 0 { t[i] } else { e[i] };
13727 }
13728 } else {
13729 let c = sl_i64(*cond, base, len);
13730 for i in 0..len {
13731 o[i] = if c[i] != 0 { t[i] } else { e[i] };
13732 }
13733 }
13734 } else if *cond_elem_bytes == 1 {
13735 let c = &arena_buf[*cond..*cond + len];
13736 let t = sl(*on_true, base, len);
13737 let e = sl(*on_false, base, len);
13738 let o = sl_mut(*dst, base, len);
13739 for i in 0..len {
13740 o[i] = if c[i] != 0 { t[i] } else { e[i] };
13741 }
13742 } else {
13743 let c = sl(*cond, base, len);
13744 let t = sl(*on_true, base, len);
13745 let e = sl(*on_false, base, len);
13746 let o = sl_mut(*dst, base, len);
13747 for i in 0..len {
13748 o[i] = if c[i] != 0.0 { t[i] } else { e[i] };
13749 }
13750 }
13751 }
13752 }
13753
13754 Thunk::Fma {
13755 a,
13756 b,
13757 c,
13758 dst,
13759 len,
13760 elem_bytes,
13761 } => {
13762 let len = *len as usize;
13763 let eb = (*elem_bytes).max(1) as usize;
13764 let arena_len = arena_buf.len();
13765 let len = len
13766 .min(arena_len.saturating_sub(*a) / eb)
13767 .min(arena_len.saturating_sub(*b) / eb)
13768 .min(arena_len.saturating_sub(*c) / eb)
13769 .min(arena_len.saturating_sub(*dst) / eb);
13770 unsafe {
13771 if *elem_bytes == 8 {
13772 let av = sl_f64(*a, base, len);
13773 let bv = sl_f64(*b, base, len);
13774 let cv = sl_f64(*c, base, len);
13775 let o = sl_mut_f64(*dst, base, len);
13776 for i in 0..len {
13777 o[i] = av[i].mul_add(bv[i], cv[i]);
13778 }
13779 } else {
13780 let av = sl(*a, base, len);
13781 let bv = sl(*b, base, len);
13782 let cv = sl(*c, base, len);
13783 let o = sl_mut(*dst, base, len);
13784 for i in 0..len {
13785 o[i] = av[i].mul_add(bv[i], cv[i]);
13786 }
13787 }
13788 }
13789 }
13790
13791 Thunk::ScatterAdd {
13792 updates,
13793 indices,
13794 dst,
13795 num_updates,
13796 out_dim,
13797 trailing,
13798 } => {
13799 let num_updates = *num_updates as usize;
13800 let out_dim = *out_dim as usize;
13801 let trailing = *trailing as usize;
13802 unsafe {
13803 let upd = sl(*updates, base, num_updates * trailing);
13804 let ids = sl(*indices, base, num_updates);
13805 let out = sl_mut(*dst, base, out_dim * trailing);
13806 for v in out.iter_mut() {
13808 *v = 0.0;
13809 }
13810 for i in 0..num_updates {
13811 let row = ids[i] as usize;
13812 debug_assert!(row < out_dim, "ScatterAdd index out of range");
13813 let src_off = i * trailing;
13814 let dst_off = row * trailing;
13815 for j in 0..trailing {
13816 out[dst_off + j] += upd[src_off + j];
13817 }
13818 }
13819 }
13820 }
13821
13822 Thunk::GroupedMatMul {
13823 input,
13824 weight,
13825 expert_idx,
13826 dst,
13827 m,
13828 k_dim,
13829 n,
13830 num_experts,
13831 } => {
13832 let m = *m as usize;
13833 let k_dim = *k_dim as usize;
13834 let n = *n as usize;
13835 let num_experts = *num_experts as usize;
13836 unsafe {
13837 let inp = sl(*input, base, m * k_dim);
13838 let wt = sl(*weight, base, num_experts * k_dim * n);
13839 let ids = sl(*expert_idx, base, m);
13840 let out = sl_mut(*dst, base, m * n);
13841
13842 let mut counts = vec![0usize; num_experts];
13845 for i in 0..m {
13846 let e = ids[i] as usize;
13847 debug_assert!(
13848 e < num_experts,
13849 "expert_idx out of range: {e} >= {num_experts}"
13850 );
13851 counts[e] += 1;
13852 }
13853 let mut offsets = vec![0usize; num_experts + 1];
13855 for e in 0..num_experts {
13856 offsets[e + 1] = offsets[e] + counts[e];
13857 }
13858 let mut packed_in = vec![0f32; m * k_dim];
13862 let mut original_pos = vec![0usize; m];
13863 let mut write_idx = vec![0usize; num_experts];
13864 for i in 0..m {
13865 let e = ids[i] as usize;
13866 let dst_row = offsets[e] + write_idx[e];
13867 packed_in[dst_row * k_dim..(dst_row + 1) * k_dim]
13868 .copy_from_slice(&inp[i * k_dim..(i + 1) * k_dim]);
13869 original_pos[dst_row] = i;
13870 write_idx[e] += 1;
13871 }
13872
13873 let mut packed_out = vec![0f32; m * n];
13877 let expert_stride = k_dim * n;
13878 let gmm_ord = crate::moe_residency::next_gmm_ord();
13879 let moe_layer = gmm_ord / 3;
13880 for e in 0..num_experts {
13881 let count = counts[e];
13882 if count == 0 {
13883 continue;
13884 }
13885 crate::moe_residency::record_expert_tokens(moe_layer, e, count);
13886 let in_start = offsets[e];
13887 let in_slice = &packed_in[in_start * k_dim..(in_start + count) * k_dim];
13888 let w_slab: &[f32] =
13889 if !crate::moe_residency::expert_on_device_for_layer(moe_layer, e) {
13890 if let Some(ptr) =
13891 crate::moe_residency::host_expert_weight_ptr(gmm_ord, e)
13892 {
13893 std::slice::from_raw_parts(ptr, expert_stride)
13894 } else {
13895 &wt[e * expert_stride..(e + 1) * expert_stride]
13896 }
13897 } else {
13898 &wt[e * expert_stride..(e + 1) * expert_stride]
13899 };
13900 let out_slice = &mut packed_out[in_start * n..(in_start + count) * n];
13901 crate::blas::sgemm(in_slice, w_slab, out_slice, count, k_dim, n);
13902 }
13903
13904 for packed_idx in 0..m {
13906 let i = original_pos[packed_idx];
13907 out[i * n..(i + 1) * n]
13908 .copy_from_slice(&packed_out[packed_idx * n..(packed_idx + 1) * n]);
13909 }
13910 }
13911 }
13912
13913 Thunk::DequantGroupedMatMulGguf {
13914 input,
13915 w_q,
13916 expert_idx,
13917 dst,
13918 m,
13919 k_dim,
13920 n,
13921 num_experts,
13922 scheme,
13923 } => {
13924 let m = *m as usize;
13925 let k_dim = *k_dim as usize;
13926 let n = *n as usize;
13927 let num_experts = *num_experts as usize;
13928 let block_elems = scheme.gguf_block_size() as usize;
13929 let block_bytes = scheme.gguf_block_bytes() as usize;
13930 let slab_bytes = (k_dim * n) / block_elems * block_bytes;
13931 unsafe {
13932 let inp = sl(*input, base, m * k_dim);
13933 let wt = std::slice::from_raw_parts(
13934 base.add(*w_q) as *const u8,
13935 num_experts * slab_bytes,
13936 );
13937 let ids = sl(*expert_idx, base, m);
13938 let out = sl_mut(*dst, base, m * n);
13939 crate::gguf_matmul::gguf_grouped_matmul_bt(
13940 inp,
13941 wt,
13942 ids,
13943 out,
13944 m,
13945 k_dim,
13946 n,
13947 num_experts,
13948 *scheme,
13949 );
13950 }
13951 }
13952
13953 Thunk::DequantMoEWeightsGguf {
13954 w_q,
13955 dst,
13956 k_dim,
13957 n,
13958 num_experts,
13959 scheme,
13960 } => {
13961 let k_dim = *k_dim as usize;
13962 let n = *n as usize;
13963 let num_experts = *num_experts as usize;
13964 let block_elems = scheme.gguf_block_size() as usize;
13965 let block_bytes = scheme.gguf_block_bytes() as usize;
13966 let slab_bytes = (k_dim * n) / block_elems * block_bytes;
13967 unsafe {
13968 let wt = std::slice::from_raw_parts(
13969 base.add(*w_q) as *const u8,
13970 num_experts * slab_bytes,
13971 );
13972 let out = sl_mut(*dst, base, num_experts * k_dim * n);
13973 crate::gguf_matmul::dequant_moe_weights_to_grouped_f32(
13974 wt,
13975 out,
13976 num_experts,
13977 k_dim,
13978 n,
13979 *scheme,
13980 );
13981 }
13982 }
13983
13984 Thunk::TopK {
13985 src,
13986 dst,
13987 outer,
13988 axis_dim,
13989 k,
13990 indices_i64,
13991 } => {
13992 let outer = *outer as usize;
13993 let axis_dim = *axis_dim as usize;
13994 let k = *k as usize;
13995 unsafe {
13996 let inp = sl(*src, base, outer * axis_dim);
13997 let mut row_buf: Vec<f32> = vec![0.0; axis_dim];
14001 if *indices_i64 != 0 {
14002 let out = sl_mut_i64(*dst, base, outer * k);
14003 for o in 0..outer {
14004 row_buf.copy_from_slice(&inp[o * axis_dim..(o + 1) * axis_dim]);
14005 for ki in 0..k {
14006 let mut best_i = 0usize;
14007 let mut best_v = row_buf[0];
14008 for i in 1..axis_dim {
14009 let v = row_buf[i];
14010 if v > best_v {
14011 best_v = v;
14012 best_i = i;
14013 }
14014 }
14015 out[o * k + ki] = best_i as i64;
14016 row_buf[best_i] = f32::NEG_INFINITY;
14017 }
14018 }
14019 } else {
14020 let out = sl_mut(*dst, base, outer * k);
14021 for o in 0..outer {
14022 row_buf.copy_from_slice(&inp[o * axis_dim..(o + 1) * axis_dim]);
14023 for ki in 0..k {
14024 let mut best_i = 0usize;
14025 let mut best_v = row_buf[0];
14026 for i in 1..axis_dim {
14027 let v = row_buf[i];
14028 if v > best_v {
14029 best_v = v;
14030 best_i = i;
14031 }
14032 }
14033 out[o * k + ki] = best_i as f32;
14034 row_buf[best_i] = f32::NEG_INFINITY;
14035 }
14036 }
14037 if let Some(cap) = schedule.moe_topk_capture.as_ref() {
14038 cap.push_topk_f32(&out[..outer * k], axis_dim);
14039 }
14040 }
14041 }
14042 }
14043
14044 Thunk::Reduce {
14045 src,
14046 dst,
14047 outer,
14048 reduced,
14049 inner,
14050 op,
14051 } => {
14052 let outer = *outer as usize;
14053 let reduced = *reduced as usize;
14054 let inner = *inner as usize;
14055 let in_total = outer * reduced * inner;
14056 let out_total = outer * inner;
14057 unsafe {
14058 let inp = sl(*src, base, in_total);
14059 let out = sl_mut(*dst, base, out_total);
14060 let reduce_one = |oi: usize| -> f32 {
14064 let o = oi / inner;
14065 let i = oi % inner;
14066 let mut acc = match op {
14067 ReduceOp::Max => f32::NEG_INFINITY,
14068 ReduceOp::Min => f32::INFINITY,
14069 ReduceOp::Prod => 1.0f32,
14070 _ => 0.0f32, };
14072 for r in 0..reduced {
14073 let v = inp[o * reduced * inner + r * inner + i];
14074 acc = match op {
14075 ReduceOp::Sum | ReduceOp::Mean => acc + v,
14076 ReduceOp::Max => acc.max(v),
14077 ReduceOp::Min => acc.min(v),
14078 ReduceOp::Prod => acc * v,
14079 };
14080 }
14081 if matches!(op, ReduceOp::Mean) {
14082 acc /= reduced as f32;
14083 }
14084 acc
14085 };
14086 if fast_conv_enabled()
14087 && crate::pool::should_parallelize(in_total)
14088 && out_total > 1
14089 {
14090 let out_addr = out.as_mut_ptr() as usize;
14091 crate::pool::par_for(
14092 out_total,
14093 crate::pool::outer_chunk(out_total),
14094 &|off, cnt| {
14095 for oi in off..off + cnt {
14096 *((out_addr as *mut f32).add(oi)) = reduce_one(oi);
14097 }
14098 },
14099 );
14100 } else {
14101 for oi in 0..out_total {
14102 out[oi] = reduce_one(oi);
14103 }
14104 }
14105 }
14106 }
14107
14108 Thunk::ArgReduce {
14109 src,
14110 dst,
14111 outer,
14112 reduced,
14113 inner,
14114 is_max,
14115 } => {
14116 let outer = *outer as usize;
14117 let reduced = *reduced as usize;
14118 let inner = *inner as usize;
14119 let in_total = outer * reduced * inner;
14120 let out_total = outer * inner;
14121 unsafe {
14122 let inp = sl(*src, base, in_total);
14123 let out = sl_mut(*dst, base, out_total);
14124 for o in 0..outer {
14125 for i in 0..inner {
14126 let mut best = inp[o * reduced * inner + i];
14127 let mut best_idx = 0usize;
14128 for r in 1..reduced {
14129 let v = inp[o * reduced * inner + r * inner + i];
14130 let better = if *is_max { v > best } else { v < best };
14131 if better {
14132 best = v;
14133 best_idx = r;
14134 }
14135 }
14136 out[o * inner + i] = best_idx as f32;
14137 }
14138 }
14139 }
14140 }
14141
14142 Thunk::Conv2D1x1 {
14143 src,
14144 weight,
14145 dst,
14146 n,
14147 c_in,
14148 c_out,
14149 hw,
14150 } => {
14151 let n = *n as usize;
14152 let c_in = *c_in as usize;
14153 let c_out = *c_out as usize;
14154 let hw = *hw as usize;
14155 unsafe {
14156 let inp = sl(*src, base, n * c_in * hw);
14157 let wt = sl(*weight, base, c_out * c_in);
14158 let out = sl_mut(*dst, base, n * c_out * hw);
14159 for ni in 0..n {
14164 let in_off = ni * c_in * hw;
14165 let out_off = ni * c_out * hw;
14166 crate::blas::sgemm(
14167 wt,
14168 &inp[in_off..in_off + c_in * hw],
14169 &mut out[out_off..out_off + c_out * hw],
14170 c_out,
14171 c_in,
14172 hw,
14173 );
14174 }
14175 }
14176 }
14177
14178 Thunk::Conv2D {
14179 src,
14180 weight,
14181 dst,
14182 n,
14183 c_in,
14184 h,
14185 w,
14186 c_out,
14187 h_out,
14188 w_out,
14189 kh,
14190 kw,
14191 sh,
14192 sw,
14193 ph,
14194 pw,
14195 dh,
14196 dw,
14197 groups,
14198 } => {
14199 let n = *n as usize;
14200 let c_in = *c_in as usize;
14201 let h = *h as usize;
14202 let w = *w as usize;
14203 let c_out = *c_out as usize;
14204 let h_out = *h_out as usize;
14205 let w_out = *w_out as usize;
14206 let kh = *kh as usize;
14207 let kw = *kw as usize;
14208 let sh = *sh as usize;
14209 let sw = *sw as usize;
14210 let ph = *ph as usize;
14211 let pw = *pw as usize;
14212 let dh = *dh as usize;
14213 let dw = *dw as usize;
14214 let groups = *groups as usize;
14215 let c_in_per_g = c_in / groups;
14216 unsafe {
14217 let inp = sl(*src, base, n * c_in * h * w);
14218 let wt = sl(*weight, base, c_out * c_in_per_g * kh * kw);
14219 let out = sl_mut(*dst, base, n * c_out * h_out * w_out);
14220 let s1_nopad = sh == 1 && sw == 1 && ph == 0 && pw == 0 && dh == 1 && dw == 1;
14229 let winograd_ok = s1_nopad && kh == 3 && kw == 3 && groups == 1;
14230 if fast_conv_enabled() && winograd_enabled() && winograd_ok {
14235 conv2d_forward_winograd(inp, wt, out, n, c_in, h, w, c_out, h_out, w_out);
14236 } else if fast_conv_enabled() && direct_conv_enabled() && s1_nopad {
14237 conv2d_forward_direct(
14238 inp, wt, out, n, c_in, h, w, c_out, h_out, w_out, kh, kw, groups,
14239 );
14240 } else if fast_conv_enabled() {
14241 conv2d_forward_im2col(
14242 inp, wt, out, n, c_in, h, w, c_out, h_out, w_out, kh, kw, sh, sw, ph,
14243 pw, dh, dw, groups,
14244 );
14245 } else {
14246 conv2d_forward_naive(
14247 inp, wt, out, n, c_in, h, w, c_out, h_out, w_out, kh, kw, sh, sw, ph,
14248 pw, dh, dw, groups,
14249 );
14250 }
14251 }
14252 }
14253
14254 Thunk::Pool2D {
14255 src,
14256 dst,
14257 n,
14258 c,
14259 h,
14260 w,
14261 h_out,
14262 w_out,
14263 kh,
14264 kw,
14265 sh,
14266 sw,
14267 ph,
14268 pw,
14269 kind,
14270 } => {
14271 let n = *n as usize;
14272 let c = *c as usize;
14273 let h = *h as usize;
14274 let w = *w as usize;
14275 let h_out = *h_out as usize;
14276 let w_out = *w_out as usize;
14277 let kh = *kh as usize;
14278 let kw = *kw as usize;
14279 let sh = *sh as usize;
14280 let sw = *sw as usize;
14281 let ph = *ph as usize;
14282 let pw = *pw as usize;
14283 let kernel_area = (kh * kw) as f32;
14284 unsafe {
14285 let inp = sl(*src, base, n * c * h * w);
14286 let out = sl_mut(*dst, base, n * c * h_out * w_out);
14287 let out_addr = out.as_mut_ptr() as usize;
14291 let is_max = matches!(kind, ReduceOp::Max);
14292 let is_mean = matches!(kind, ReduceOp::Mean);
14293 let nopad = ph == 0 && pw == 0;
14297 let pool_plane = |nc: usize| {
14298 let ni = nc / c;
14299 let ci = nc % c;
14300 let in_chan = ni * c * h * w + ci * h * w;
14301 let out_chan = ni * c * h_out * w_out + ci * h_out * w_out;
14302 let op = out_addr as *mut f32;
14303 for ho in 0..h_out {
14304 for wo in 0..w_out {
14305 let acc = if nopad {
14306 let row0 = in_chan + (ho * sh) * w + wo * sw;
14307 let mut a = if is_max { f32::NEG_INFINITY } else { 0.0 };
14308 for ki in 0..kh {
14309 let row = row0 + ki * w;
14310 if is_max {
14311 for kj in 0..kw {
14312 a = a.max(inp[row + kj]);
14313 }
14314 } else {
14315 for kj in 0..kw {
14316 a += inp[row + kj];
14317 }
14318 }
14319 }
14320 a
14321 } else {
14322 let mut a = if is_max { f32::NEG_INFINITY } else { 0.0 };
14323 for ki in 0..kh {
14324 for kj in 0..kw {
14325 let hi = ho * sh + ki;
14326 let wi = wo * sw + kj;
14327 if hi < ph || wi < pw {
14328 continue;
14329 }
14330 let hi = hi - ph;
14331 let wi = wi - pw;
14332 if hi >= h || wi >= w {
14333 continue;
14334 }
14335 let v = inp[in_chan + hi * w + wi];
14336 if is_max {
14337 a = a.max(v);
14338 } else {
14339 a += v;
14340 }
14341 }
14342 }
14343 a
14344 };
14345 let acc = if is_mean { acc / kernel_area } else { acc };
14346 *op.add(out_chan + ho * w_out + wo) = acc;
14347 }
14348 }
14349 };
14350 if fast_conv_enabled() && crate::pool::should_parallelize(n * c * h_out * w_out)
14351 {
14352 crate::pool::par_for(
14353 n * c,
14354 crate::pool::outer_chunk(n * c),
14355 &|off, cnt| {
14356 for nc in off..off + cnt {
14357 pool_plane(nc);
14358 }
14359 },
14360 );
14361 } else {
14362 for nc in 0..n * c {
14363 pool_plane(nc);
14364 }
14365 }
14366 }
14367 }
14368
14369 Thunk::ReluBackward { x, dy, dx, len } => {
14370 let len = *len as usize;
14371 unsafe {
14372 let xs = sl(*x, base, len);
14373 let dys = sl(*dy, base, len);
14374 let out = sl_mut(*dx, base, len);
14375 if fast_conv_enabled() && crate::pool::should_parallelize(len) {
14376 let oa = out.as_mut_ptr() as usize;
14377 crate::pool::par_for(len, crate::pool::chunk_floor(len), &|off, cnt| {
14378 for i in off..off + cnt {
14379 *((oa as *mut f32).add(i)) = if xs[i] > 0.0 { dys[i] } else { 0.0 };
14380 }
14381 });
14382 } else {
14383 for i in 0..len {
14384 out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
14385 }
14386 }
14387 }
14388 }
14389
14390 Thunk::ReluBackwardF64 { x, dy, dx, len } => {
14391 let len = *len as usize;
14392 unsafe {
14393 let xs = sl_f64(*x, base, len);
14394 let dys = sl_f64(*dy, base, len);
14395 let out = sl_mut_f64(*dx, base, len);
14396 for i in 0..len {
14397 out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
14398 }
14399 }
14400 }
14401
14402 Thunk::QMatMul {
14403 x,
14404 w,
14405 bias,
14406 out,
14407 m,
14408 k,
14409 n,
14410 x_zp,
14411 w_zp,
14412 out_zp,
14413 mult,
14414 } => {
14415 let m = *m as usize;
14416 let k = *k as usize;
14417 let n = *n as usize;
14418 unsafe {
14419 let x_ptr = base.add(*x) as *const i8;
14420 let w_ptr = base.add(*w) as *const i8;
14421 let bias_ptr = base.add(*bias) as *const i32;
14422 let out_ptr = base.add(*out) as *mut i8;
14423 for mi in 0..m {
14424 for ni in 0..n {
14425 let mut acc: i32 = *bias_ptr.add(ni);
14426 for ki in 0..k {
14427 let xv = *x_ptr.add(mi * k + ki) as i32 - *x_zp;
14428 let wv = *w_ptr.add(ki * n + ni) as i32 - *w_zp;
14429 acc += xv * wv;
14430 }
14431 let r = (acc as f32 * *mult).round() as i32 + *out_zp;
14434 let r = r.clamp(-128, 127) as i8;
14435 *out_ptr.add(mi * n + ni) = r;
14436 }
14437 }
14438 }
14439 }
14440
14441 Thunk::QConv2d {
14442 x,
14443 w,
14444 bias,
14445 out,
14446 n,
14447 c_in,
14448 h,
14449 w_in,
14450 c_out,
14451 h_out,
14452 w_out,
14453 kh,
14454 kw,
14455 sh,
14456 sw,
14457 ph,
14458 pw,
14459 dh,
14460 dw,
14461 groups,
14462 x_zp,
14463 w_zp,
14464 out_zp,
14465 mult,
14466 } => {
14467 let n = *n as usize;
14468 let c_in = *c_in as usize;
14469 let h = *h as usize;
14470 let w_in = *w_in as usize;
14471 let c_out = *c_out as usize;
14472 let h_out = *h_out as usize;
14473 let w_out = *w_out as usize;
14474 let kh = *kh as usize;
14475 let kw = *kw as usize;
14476 let sh = *sh as usize;
14477 let sw = *sw as usize;
14478 let ph = *ph as usize;
14479 let pw = *pw as usize;
14480 let dh = *dh as usize;
14481 let dw = *dw as usize;
14482 let groups = *groups as usize;
14483 let c_in_per_g = c_in / groups;
14484 let c_out_per_g = c_out / groups;
14485 unsafe {
14486 let x_ptr = base.add(*x) as *const i8;
14487 let w_ptr = base.add(*w) as *const i8;
14488 let bias_ptr = base.add(*bias) as *const i32;
14489 let out_ptr = base.add(*out) as *mut i8;
14490 for ni in 0..n {
14491 for co in 0..c_out {
14492 let g = co / c_out_per_g;
14493 let ci_start = g * c_in_per_g;
14494 for ho in 0..h_out {
14495 for wo in 0..w_out {
14496 let mut acc: i32 = *bias_ptr.add(co);
14497 for ci_off in 0..c_in_per_g {
14498 let ci = ci_start + ci_off;
14499 let in_chan = ((ni * c_in) + ci) * h * w_in;
14500 let wt_chan = ((co * c_in_per_g) + ci_off) * kh * kw;
14501 for ki in 0..kh {
14502 for kj in 0..kw {
14503 let hi = ho * sh + ki * dh;
14504 let wi = wo * sw + kj * dw;
14505 if hi < ph || wi < pw {
14506 continue;
14507 }
14508 let hi = hi - ph;
14509 let wi = wi - pw;
14510 if hi >= h || wi >= w_in {
14511 continue;
14512 }
14513 let xv = *x_ptr.add(in_chan + hi * w_in + wi)
14514 as i32
14515 - *x_zp;
14516 let wv = *w_ptr.add(wt_chan + ki * kw + kj) as i32
14517 - *w_zp;
14518 acc += xv * wv;
14519 }
14520 }
14521 }
14522 let r = (acc as f32 * *mult).round() as i32 + *out_zp;
14523 let r = r.clamp(-128, 127) as i8;
14524 let dst = ((ni * c_out) + co) * h_out * w_out + ho * w_out + wo;
14525 *out_ptr.add(dst) = r;
14526 }
14527 }
14528 }
14529 }
14530 }
14531 }
14532
14533 Thunk::Quantize {
14534 x,
14535 q,
14536 len,
14537 chan_axis: _,
14538 chan_dim,
14539 inner,
14540 scales,
14541 zero_points,
14542 } => {
14543 let len = *len as usize;
14544 let chan_dim = *chan_dim as usize;
14545 let inner = *inner as usize;
14546 unsafe {
14547 let xs = sl(*x, base, len);
14548 let q_ptr = base.add(*q) as *mut i8;
14549 for i in 0..len {
14550 let c = if chan_dim == 1 {
14551 0
14552 } else {
14553 (i / inner) % chan_dim
14554 };
14555 let inv_scale = 1.0 / scales[c];
14556 let zp = zero_points[c];
14557 let v = (xs[i] * inv_scale).round() as i32 + zp;
14558 *q_ptr.add(i) = v.clamp(-128, 127) as i8;
14559 }
14560 }
14561 }
14562
14563 Thunk::Dequantize {
14564 q,
14565 x,
14566 len,
14567 chan_axis: _,
14568 chan_dim,
14569 inner,
14570 scales,
14571 zero_points,
14572 } => {
14573 let len = *len as usize;
14574 let chan_dim = *chan_dim as usize;
14575 let inner = *inner as usize;
14576 unsafe {
14577 let q_ptr = base.add(*q) as *const i8;
14578 let out = sl_mut(*x, base, len);
14579 for i in 0..len {
14580 let c = if chan_dim == 1 {
14581 0
14582 } else {
14583 (i / inner) % chan_dim
14584 };
14585 let scale = scales[c];
14586 let zp = zero_points[c];
14587 let qv = *q_ptr.add(i) as i32;
14588 out[i] = (qv - zp) as f32 * scale;
14589 }
14590 }
14591 }
14592
14593 Thunk::FakeQuantize {
14594 x,
14595 out,
14596 len,
14597 chan_axis: _,
14598 chan_dim,
14599 inner,
14600 bits,
14601 ste: _,
14602 scale_mode,
14603 state_off,
14604 } => {
14605 use rlx_ir::op::ScaleMode;
14606 let len = *len as usize;
14607 let chan_dim = *chan_dim as usize;
14608 let inner = *inner as usize;
14609 let q_max: f32 = match *bits {
14610 8 => 127.0,
14611 4 => 7.0,
14612 2 => 1.0,
14613 n => panic!("FakeQuantize: unsupported bits {n}"),
14614 };
14615 unsafe {
14616 let xs = sl(*x, base, len);
14617 let outs = sl_mut(*out, base, len);
14618
14619 let mut scale = vec![0f32; chan_dim];
14620 match scale_mode {
14621 ScaleMode::PerBatch => {
14622 let mut max_abs = vec![0f32; chan_dim];
14623 for i in 0..len {
14624 let c = if chan_dim == 1 {
14625 0
14626 } else {
14627 (i / inner) % chan_dim
14628 };
14629 let a = xs[i].abs();
14630 if a > max_abs[c] {
14631 max_abs[c] = a;
14632 }
14633 }
14634 for c in 0..chan_dim {
14635 scale[c] = (max_abs[c] / q_max).max(1e-12);
14636 }
14637 }
14638 ScaleMode::EMA { decay } => {
14639 let mut max_abs = vec![0f32; chan_dim];
14642 for i in 0..len {
14643 let c = if chan_dim == 1 {
14644 0
14645 } else {
14646 (i / inner) % chan_dim
14647 };
14648 let a = xs[i].abs();
14649 if a > max_abs[c] {
14650 max_abs[c] = a;
14651 }
14652 }
14653 let state =
14654 sl_mut(state_off.expect("EMA needs state_off"), base, chan_dim);
14655 for c in 0..chan_dim {
14656 let cur = (max_abs[c] / q_max).max(1e-12);
14657 let blended = if state[c] <= 0.0 {
14659 cur
14660 } else {
14661 *decay * state[c] + (1.0 - *decay) * cur
14662 };
14663 state[c] = blended;
14664 scale[c] = blended;
14665 }
14666 }
14667 ScaleMode::Fixed => {
14668 let state =
14669 sl(state_off.expect("Fixed needs state_off"), base, chan_dim);
14670 for c in 0..chan_dim {
14671 scale[c] = state[c].max(1e-12);
14672 }
14673 }
14674 }
14675
14676 for i in 0..len {
14677 let c = if chan_dim == 1 {
14678 0
14679 } else {
14680 (i / inner) % chan_dim
14681 };
14682 let s = scale[c];
14683 let qv = (xs[i] / s).round().clamp(-q_max, q_max);
14684 outs[i] = qv * s;
14685 }
14686 }
14687 }
14688
14689 Thunk::ActivationBackward {
14690 x,
14691 dy,
14692 dx,
14693 len,
14694 kind,
14695 } => {
14696 let len = *len as usize;
14697 unsafe {
14698 let xs = sl(*x, base, len);
14699 let dys = sl(*dy, base, len);
14700 let out = sl_mut(*dx, base, len);
14701 activation_backward_kernel(*kind, xs, dys, out);
14702 }
14703 }
14704
14705 Thunk::ActivationBackwardF64 {
14706 x,
14707 dy,
14708 dx,
14709 len,
14710 kind,
14711 } => {
14712 let len = *len as usize;
14713 unsafe {
14714 let xs = sl_f64(*x, base, len);
14715 let dys = sl_f64(*dy, base, len);
14716 let out = sl_mut_f64(*dx, base, len);
14717 activation_backward_kernel_f64(*kind, xs, dys, out);
14718 }
14719 }
14720
14721 Thunk::FakeQuantizeLSQ {
14722 x,
14723 scale_off,
14724 out,
14725 len,
14726 chan_axis: _,
14727 chan_dim,
14728 inner,
14729 bits,
14730 } => {
14731 let len = *len as usize;
14732 let chan_dim = *chan_dim as usize;
14733 let inner = *inner as usize;
14734 let q_max: f32 = match *bits {
14735 8 => 127.0,
14736 4 => 7.0,
14737 2 => 1.0,
14738 n => panic!("FakeQuantizeLSQ: bad bits {n}"),
14739 };
14740 unsafe {
14741 let xs = sl(*x, base, len);
14742 let scale = sl(*scale_off, base, chan_dim);
14743 let outs = sl_mut(*out, base, len);
14744 for i in 0..len {
14745 let c = if chan_dim == 1 {
14746 0
14747 } else {
14748 (i / inner) % chan_dim
14749 };
14750 let s = scale[c].max(1e-12);
14751 let qv = (xs[i] / s).round().clamp(-q_max, q_max);
14752 outs[i] = qv * s;
14753 }
14754 }
14755 }
14756
14757 Thunk::FakeQuantizeLSQBackwardX {
14758 x,
14759 scale_off,
14760 dy,
14761 dx,
14762 len,
14763 chan_axis: _,
14764 chan_dim,
14765 inner,
14766 bits,
14767 } => {
14768 let len = *len as usize;
14769 let chan_dim = *chan_dim as usize;
14770 let inner = *inner as usize;
14771 let q_max: f32 = match *bits {
14772 8 => 127.0,
14773 4 => 7.0,
14774 2 => 1.0,
14775 n => panic!("FakeQuantizeLSQBackwardX: bad bits {n}"),
14776 };
14777 unsafe {
14778 let xs = sl(*x, base, len);
14779 let scale = sl(*scale_off, base, chan_dim);
14780 let dys = sl(*dy, base, len);
14781 let outs = sl_mut(*dx, base, len);
14782 for i in 0..len {
14784 let c = if chan_dim == 1 {
14785 0
14786 } else {
14787 (i / inner) % chan_dim
14788 };
14789 let z = xs[i] / scale[c].max(1e-12);
14790 outs[i] = if z.abs() <= q_max { dys[i] } else { 0.0 };
14791 }
14792 }
14793 }
14794
14795 Thunk::FakeQuantizeLSQBackwardScale {
14796 x,
14797 scale_off,
14798 dy,
14799 dscale,
14800 len,
14801 chan_axis: _,
14802 chan_dim,
14803 inner,
14804 bits,
14805 } => {
14806 let len = *len as usize;
14807 let chan_dim = *chan_dim as usize;
14808 let inner = *inner as usize;
14809 let q_max: f32 = match *bits {
14810 8 => 127.0,
14811 4 => 7.0,
14812 2 => 1.0,
14813 n => panic!("FakeQuantizeLSQBackwardScale: bad bits {n}"),
14814 };
14815 unsafe {
14816 let xs = sl(*x, base, len);
14817 let scale = sl(*scale_off, base, chan_dim);
14818 let dys = sl(*dy, base, len);
14819 let outs = sl_mut(*dscale, base, chan_dim);
14820 for v in outs.iter_mut() {
14821 *v = 0.0;
14822 }
14823 for i in 0..len {
14826 let c = if chan_dim == 1 {
14827 0
14828 } else {
14829 (i / inner) % chan_dim
14830 };
14831 let s = scale[c].max(1e-12);
14832 let z = xs[i] / s;
14833 let psi = if z.abs() <= q_max {
14834 -z + z.round()
14835 } else if z > 0.0 {
14836 q_max
14837 } else {
14838 -q_max
14839 };
14840 outs[c] += psi * dys[i];
14841 }
14842 }
14843 }
14844
14845 Thunk::FakeQuantizeBackward {
14846 x,
14847 dy,
14848 dx,
14849 len,
14850 chan_axis: _,
14851 chan_dim,
14852 inner,
14853 bits,
14854 ste,
14855 } => {
14856 use rlx_ir::op::SteKind;
14857 let len = *len as usize;
14858 let chan_dim = *chan_dim as usize;
14859 let inner = *inner as usize;
14860 let q_max: f32 = match *bits {
14861 8 => 127.0,
14862 4 => 7.0,
14863 2 => 1.0,
14864 n => panic!("FakeQuantizeBackward: bad bits {n}"),
14865 };
14866 unsafe {
14867 let xs = sl(*x, base, len);
14868 let dys = sl(*dy, base, len);
14869 let outs = sl_mut(*dx, base, len);
14870
14871 let mut max_abs = vec![0f32; chan_dim];
14873 for i in 0..len {
14874 let c = if chan_dim == 1 {
14875 0
14876 } else {
14877 (i / inner) % chan_dim
14878 };
14879 let a = xs[i].abs();
14880 if a > max_abs[c] {
14881 max_abs[c] = a;
14882 }
14883 }
14884 let mut scale = vec![0f32; chan_dim];
14885 for c in 0..chan_dim {
14886 scale[c] = (max_abs[c] / q_max).max(1e-12);
14887 }
14888
14889 match *ste {
14890 SteKind::Identity => {
14891 outs.copy_from_slice(dys);
14893 }
14894 SteKind::ClippedIdentity => {
14895 for i in 0..len {
14898 let c = if chan_dim == 1 {
14899 0
14900 } else {
14901 (i / inner) % chan_dim
14902 };
14903 let bound = q_max * scale[c];
14904 outs[i] = if xs[i].abs() <= bound { dys[i] } else { 0.0 };
14905 }
14906 }
14907 SteKind::Tanh => {
14908 for i in 0..len {
14910 let c = if chan_dim == 1 {
14911 0
14912 } else {
14913 (i / inner) % chan_dim
14914 };
14915 let t = (xs[i] / scale[c]).tanh();
14916 outs[i] = dys[i] * (1.0 - t * t);
14917 }
14918 }
14919 SteKind::HardTanh => {
14920 for i in 0..len {
14922 let c = if chan_dim == 1 {
14923 0
14924 } else {
14925 (i / inner) % chan_dim
14926 };
14927 let bound = q_max * scale[c];
14928 let attenuation = (1.0 - (xs[i] / bound).abs()).max(0.0);
14929 outs[i] = dys[i] * attenuation;
14930 }
14931 }
14932 }
14933 }
14934 }
14935
14936 Thunk::LayerNormBackwardInput {
14937 x,
14938 gamma,
14939 dy,
14940 dx,
14941 rows,
14942 h,
14943 eps,
14944 } => {
14945 let rows = *rows as usize;
14946 let h = *h as usize;
14947 let eps = *eps;
14948 unsafe {
14949 let xs = sl(*x, base, rows * h);
14950 let g = sl(*gamma, base, h);
14951 let dys = sl(*dy, base, rows * h);
14952 let out = sl_mut(*dx, base, rows * h);
14953 let n_inv = 1.0 / h as f32;
14954 for r in 0..rows {
14955 let xr = &xs[r * h..(r + 1) * h];
14956 let dyr = &dys[r * h..(r + 1) * h];
14957 let mut sum = 0f32;
14960 for &v in xr {
14961 sum += v;
14962 }
14963 let mean = sum * n_inv;
14964 let mut var = 0f32;
14965 for &v in xr {
14966 let d = v - mean;
14967 var += d * d;
14968 }
14969 let inv_std = 1.0 / (var * n_inv + eps).sqrt();
14970
14971 let mut s_sy = 0f32;
14974 let mut s_sxh = 0f32;
14975 for d in 0..h {
14976 let xh = (xr[d] - mean) * inv_std;
14977 let sy = dyr[d] * g[d];
14978 s_sy += sy;
14979 s_sxh += sy * xh;
14980 }
14981 let m_sy = s_sy * n_inv;
14982 let m_sxh = s_sxh * n_inv;
14983
14984 for d in 0..h {
14985 let xh = (xr[d] - mean) * inv_std;
14986 let sy = dyr[d] * g[d];
14987 out[r * h + d] = inv_std * (sy - m_sy - xh * m_sxh);
14988 }
14989 }
14990 }
14991 }
14992
14993 Thunk::BatchNormInferenceBackwardInput {
14994 x,
14995 gamma,
14996 mean,
14997 var,
14998 dy,
14999 dx,
15000 count,
15001 channels,
15002 eps,
15003 } => {
15004 let count = *count as usize;
15005 let c = *channels as usize;
15006 let n = count * c;
15007 let eps = *eps;
15008 unsafe {
15009 crate::kernels::batch_norm_inference_backward_input(
15010 sl(*x, base, n),
15011 sl(*gamma, base, c),
15012 sl(*mean, base, c),
15013 sl(*var, base, c),
15014 sl(*dy, base, n),
15015 sl_mut(*dx, base, n),
15016 c,
15017 eps,
15018 );
15019 }
15020 }
15021
15022 Thunk::BatchNormInferenceBackwardGamma {
15023 x,
15024 mean,
15025 var,
15026 dy,
15027 dgamma,
15028 count,
15029 channels,
15030 eps,
15031 } => {
15032 let count = *count as usize;
15033 let c = *channels as usize;
15034 let n = count * c;
15035 let eps = *eps;
15036 unsafe {
15037 crate::kernels::batch_norm_inference_backward_gamma(
15038 sl(*x, base, n),
15039 sl(*mean, base, c),
15040 sl(*var, base, c),
15041 sl(*dy, base, n),
15042 sl_mut(*dgamma, base, c),
15043 c,
15044 eps,
15045 );
15046 }
15047 }
15048
15049 Thunk::BatchNormInferenceBackwardBeta {
15050 dy,
15051 dbeta,
15052 count,
15053 channels,
15054 } => {
15055 let count = *count as usize;
15056 let c = *channels as usize;
15057 let n = count * c;
15058 unsafe {
15059 crate::kernels::batch_norm_inference_backward_beta(
15060 sl(*dy, base, n),
15061 sl_mut(*dbeta, base, c),
15062 c,
15063 );
15064 }
15065 }
15066
15067 Thunk::LayerNormBackwardGamma {
15068 x,
15069 dy,
15070 dgamma,
15071 rows,
15072 h,
15073 eps,
15074 } => {
15075 let rows = *rows as usize;
15076 let h = *h as usize;
15077 let eps = *eps;
15078 unsafe {
15079 let xs = sl(*x, base, rows * h);
15080 let dys = sl(*dy, base, rows * h);
15081 let out = sl_mut(*dgamma, base, h);
15082 for v in out.iter_mut() {
15083 *v = 0.0;
15084 }
15085 let n_inv = 1.0 / h as f32;
15086 for r in 0..rows {
15087 let xr = &xs[r * h..(r + 1) * h];
15088 let dyr = &dys[r * h..(r + 1) * h];
15089 let mut sum = 0f32;
15090 for &v in xr {
15091 sum += v;
15092 }
15093 let mean = sum * n_inv;
15094 let mut var = 0f32;
15095 for &v in xr {
15096 let d = v - mean;
15097 var += d * d;
15098 }
15099 let inv_std = 1.0 / (var * n_inv + eps).sqrt();
15100 for d in 0..h {
15101 let xh = (xr[d] - mean) * inv_std;
15102 out[d] += dyr[d] * xh;
15103 }
15104 }
15105 }
15106 }
15107
15108 Thunk::RmsNormBackwardInput {
15109 x,
15110 gamma,
15111 beta,
15112 dy,
15113 dx,
15114 rows,
15115 h,
15116 eps,
15117 } => {
15118 let (rows, h) = (*rows as usize, *h as usize);
15119 unsafe {
15120 let xs = sl(*x, base, rows * h);
15121 let g = sl(*gamma, base, h);
15122 let b = sl(*beta, base, h);
15123 let dys = sl(*dy, base, rows * h);
15124 let out = sl_mut(*dx, base, rows * h);
15125 let mut dg = vec![0f32; h];
15126 let mut db = vec![0f32; h];
15127 for r in 0..rows {
15128 crate::training_bwd::rms_norm_backward_row(
15129 &xs[r * h..(r + 1) * h],
15130 g,
15131 b,
15132 &dys[r * h..(r + 1) * h],
15133 &mut out[r * h..(r + 1) * h],
15134 &mut dg,
15135 &mut db,
15136 *eps,
15137 );
15138 }
15139 }
15140 }
15141
15142 Thunk::RmsNormBackwardGamma {
15143 x,
15144 gamma,
15145 beta,
15146 dy,
15147 dgamma,
15148 rows,
15149 h,
15150 eps,
15151 } => {
15152 let (rows, h) = (*rows as usize, *h as usize);
15153 unsafe {
15154 let xs = sl(*x, base, rows * h);
15155 let g = sl(*gamma, base, h);
15156 let b = sl(*beta, base, h);
15157 let dys = sl(*dy, base, rows * h);
15158 let out = sl_mut(*dgamma, base, h);
15159 for v in out.iter_mut() {
15160 *v = 0.0;
15161 }
15162 let mut dx = vec![0f32; h];
15163 let mut db = vec![0f32; h];
15164 for r in 0..rows {
15165 crate::training_bwd::rms_norm_backward_row(
15166 &xs[r * h..(r + 1) * h],
15167 g,
15168 b,
15169 &dys[r * h..(r + 1) * h],
15170 &mut dx,
15171 &mut *out,
15172 &mut db,
15173 *eps,
15174 );
15175 }
15176 }
15177 }
15178
15179 Thunk::RmsNormBackwardBeta {
15180 x,
15181 gamma,
15182 beta,
15183 dy,
15184 dbeta,
15185 rows,
15186 h,
15187 eps,
15188 } => {
15189 let (rows, h) = (*rows as usize, *h as usize);
15190 unsafe {
15191 let xs = sl(*x, base, rows * h);
15192 let g = sl(*gamma, base, h);
15193 let b = sl(*beta, base, h);
15194 let dys = sl(*dy, base, rows * h);
15195 let out = sl_mut(*dbeta, base, h);
15196 for v in out.iter_mut() {
15197 *v = 0.0;
15198 }
15199 let mut dx = vec![0f32; h];
15200 let mut dg = vec![0f32; h];
15201 for r in 0..rows {
15202 crate::training_bwd::rms_norm_backward_row(
15203 &xs[r * h..(r + 1) * h],
15204 g,
15205 b,
15206 &dys[r * h..(r + 1) * h],
15207 &mut dx,
15208 &mut dg,
15209 &mut *out,
15210 *eps,
15211 );
15212 }
15213 }
15214 }
15215
15216 Thunk::RopeBackward {
15217 dy,
15218 cos,
15219 sin,
15220 dx,
15221 batch,
15222 seq,
15223 hidden,
15224 head_dim,
15225 n_rot,
15226 cos_len,
15227 } => {
15228 let (b, s, hs, dh, nr, cl) = (
15229 *batch as usize,
15230 *seq as usize,
15231 *hidden as usize,
15232 *head_dim as usize,
15233 *n_rot as usize,
15234 *cos_len as usize,
15235 );
15236 let nh = hs / dh;
15237 let tab_half = dh / 2;
15238 unsafe {
15239 let dys = sl(*dy, base, b * s * hs);
15240 let cos_tab = sl(*cos, base, cl);
15241 let sin_tab = sl(*sin, base, cl);
15242 let out = sl_mut(*dx, base, b * s * hs);
15243 for bi in 0..b {
15244 for si in 0..s {
15245 let tab_off = si.saturating_mul(tab_half) % cl.max(1);
15246 let cp = &cos_tab[tab_off..tab_off + tab_half.min(cl)];
15247 let sp = &sin_tab[tab_off..tab_off + tab_half.min(cl)];
15248 for hi in 0..nh {
15249 let base_idx = bi * s * hs + si * hs + hi * dh;
15250 crate::training_bwd::rope_backward_row(
15251 &dys[base_idx..base_idx + dh],
15252 cp,
15253 sp,
15254 &mut out[base_idx..base_idx + dh],
15255 dh,
15256 nr,
15257 );
15258 }
15259 }
15260 }
15261 }
15262 }
15263
15264 Thunk::CumsumBackward {
15265 dy,
15266 dx,
15267 rows,
15268 cols,
15269 exclusive,
15270 } => {
15271 let (rows, cols) = (*rows as usize, *cols as usize);
15272 unsafe {
15273 let dys = sl(*dy, base, rows * cols);
15274 let out = sl_mut(*dx, base, rows * cols);
15275 for r in 0..rows {
15276 crate::training_bwd::cumsum_backward_row(
15277 &dys[r * cols..(r + 1) * cols],
15278 &mut out[r * cols..(r + 1) * cols],
15279 *exclusive,
15280 );
15281 }
15282 }
15283 }
15284
15285 Thunk::GroupNormBackwardInput {
15286 x,
15287 gamma,
15288 beta: _beta,
15289 dy,
15290 dx,
15291 n,
15292 c,
15293 h,
15294 w,
15295 num_groups,
15296 eps,
15297 } => {
15298 let (n, c, h, w) = (*n as usize, *c as usize, *h as usize, *w as usize);
15299 let plane = c * h * w;
15300 unsafe {
15301 let xs = sl(*x, base, n * plane);
15302 let g = sl(*gamma, base, c);
15303 let dys = sl(*dy, base, n * plane);
15304 let out = sl_mut(*dx, base, n * plane);
15305 crate::training_bwd::group_norm_backward_input_nchw(
15306 xs,
15307 g,
15308 dys,
15309 out,
15310 n,
15311 c,
15312 h,
15313 w,
15314 *num_groups as usize,
15315 *eps,
15316 );
15317 }
15318 }
15319
15320 Thunk::GroupNormBackwardGamma {
15321 x,
15322 dy,
15323 dgamma,
15324 n,
15325 c,
15326 h,
15327 w,
15328 num_groups,
15329 eps,
15330 } => {
15331 let (n, c, h, w) = (*n as usize, *c as usize, *h as usize, *w as usize);
15332 let plane = c * h * w;
15333 unsafe {
15334 let xs = sl(*x, base, n * plane);
15335 let dys = sl(*dy, base, n * plane);
15336 let out = sl_mut(*dgamma, base, c);
15337 crate::training_bwd::group_norm_backward_gamma_nchw(
15338 xs,
15339 dys,
15340 out,
15341 n,
15342 c,
15343 h,
15344 w,
15345 *num_groups as usize,
15346 *eps,
15347 );
15348 }
15349 }
15350
15351 Thunk::GroupNormBackwardBeta {
15352 dy,
15353 dbeta,
15354 n,
15355 c,
15356 h,
15357 w,
15358 } => {
15359 let (n, c, h, w) = (*n as usize, *c as usize, *h as usize, *w as usize);
15360 let plane = c * h * w;
15361 unsafe {
15362 let dys = sl(*dy, base, n * plane);
15363 let out = sl_mut(*dbeta, base, c);
15364 crate::training_bwd::group_norm_backward_beta_nchw(dys, out, n, c, h, w);
15365 }
15366 }
15367
15368 Thunk::GatherBackward {
15369 dy,
15370 indices,
15371 dst,
15372 outer,
15373 axis_dim,
15374 num_idx,
15375 trailing,
15376 } => {
15377 let (outer, axis_dim, num_idx, trailing) = (
15378 *outer as usize,
15379 *axis_dim as usize,
15380 *num_idx as usize,
15381 *trailing as usize,
15382 );
15383 unsafe {
15384 let dys = sl(*dy, base, outer * num_idx * trailing);
15385 let ids = sl(*indices, base, num_idx);
15386 let out = sl_mut(*dst, base, outer * axis_dim * trailing);
15387 for v in out.iter_mut() {
15388 *v = 0.0;
15389 }
15390 crate::training_bwd::gather_axis_backward(
15391 dys, ids, out, outer, axis_dim, num_idx, trailing,
15392 );
15393 }
15394 }
15395
15396 Thunk::MaxPool2dBackward {
15397 x,
15398 dy,
15399 dx,
15400 n,
15401 c,
15402 h,
15403 w,
15404 h_out,
15405 w_out,
15406 kh,
15407 kw,
15408 sh,
15409 sw,
15410 ph,
15411 pw,
15412 } => unsafe {
15413 execute_maxpool2d_backward_f32(
15414 *x, *dy, *dx, *n, *c, *h, *w, *h_out, *w_out, *kh, *kw, *sh, *sw, *ph, *pw,
15415 base,
15416 );
15417 },
15418
15419 Thunk::Conv2dBackwardInput {
15420 dy,
15421 w,
15422 dx,
15423 n,
15424 c_in,
15425 h,
15426 w_in,
15427 c_out,
15428 h_out,
15429 w_out,
15430 kh,
15431 kw,
15432 sh,
15433 sw,
15434 ph,
15435 pw,
15436 dh,
15437 dw,
15438 groups,
15439 } => {
15440 let n = *n as usize;
15452 let c_in = *c_in as usize;
15453 let h = *h as usize;
15454 let w_in = *w_in as usize;
15455 let c_out = *c_out as usize;
15456 let h_out = *h_out as usize;
15457 let w_out = *w_out as usize;
15458 let kh = *kh as usize;
15459 let kw = *kw as usize;
15460 let sh = *sh as usize;
15461 let sw = *sw as usize;
15462 let ph = *ph as usize;
15463 let pw = *pw as usize;
15464 let dh = *dh as usize;
15465 let dw = *dw as usize;
15466 let groups = *groups as usize;
15467 let c_in_per_g = c_in / groups;
15468 let c_out_per_g = c_out / groups;
15469
15470 let m_dim = c_in_per_g * kh * kw;
15471 let n_dim = h_out * w_out;
15472 let k_dim = c_out_per_g;
15473
15474 let dy_stride_n = c_out * h_out * w_out;
15475 let dy_stride_g = c_out_per_g * h_out * w_out;
15476 let w_stride_g = c_out_per_g * c_in_per_g * kh * kw;
15477 let dx_stride_n = c_in * h * w_in;
15478 let dx_stride_g = c_in_per_g * h * w_in;
15479
15480 unsafe {
15481 let dys = sl(*dy, base, n * c_out * h_out * w_out);
15482 let ws = sl(*w, base, c_out * c_in_per_g * kh * kw);
15483 let dxs = sl_mut(*dx, base, n * c_in * h * w_in);
15484 for v in dxs.iter_mut() {
15485 *v = 0.0;
15486 }
15487
15488 if fast_conv_enabled() {
15494 let dx_addr = dxs.as_mut_ptr() as usize;
15495 crate::pool::par_for(n, 1, &|off, cnt| {
15496 let mut dcol = vec![0f32; m_dim * n_dim];
15497 for ni in off..off + cnt {
15498 for g in 0..groups {
15499 let w_g_off = g * w_stride_g;
15500 let dy_n_g_off = ni * dy_stride_n + g * dy_stride_g;
15501 let dx_n_g_off = ni * dx_stride_n + g * dx_stride_g;
15502 crate::blas::sgemm_general(
15503 ws.as_ptr().add(w_g_off),
15504 dys.as_ptr().add(dy_n_g_off),
15505 dcol.as_mut_ptr(),
15506 m_dim,
15507 n_dim,
15508 k_dim,
15509 1.0,
15510 0.0,
15511 m_dim,
15512 n_dim,
15513 n_dim,
15514 true,
15515 false,
15516 );
15517 let dx_g = std::slice::from_raw_parts_mut(
15518 (dx_addr as *mut f32).add(dx_n_g_off),
15519 dx_stride_g,
15520 );
15521 col2im(
15522 &dcol, dx_g, c_in_per_g, h, w_in, h_out, w_out, kh, kw, sh,
15523 sw, ph, pw, dh, dw,
15524 );
15525 }
15526 }
15527 });
15528 } else {
15529 let mut dcol = vec![0f32; m_dim * n_dim];
15531 for ni in 0..n {
15532 for g in 0..groups {
15533 let w_g_off = g * w_stride_g;
15534 let dy_n_g_off = ni * dy_stride_n + g * dy_stride_g;
15535 let dx_n_g_off = ni * dx_stride_n + g * dx_stride_g;
15536
15537 crate::blas::sgemm_general(
15542 ws.as_ptr().add(w_g_off),
15543 dys.as_ptr().add(dy_n_g_off),
15544 dcol.as_mut_ptr(),
15545 m_dim,
15546 n_dim,
15547 k_dim,
15548 1.0,
15549 0.0,
15550 m_dim,
15551 n_dim,
15552 n_dim,
15553 true,
15554 false,
15555 );
15556
15557 col2im(
15559 &dcol,
15560 &mut dxs[dx_n_g_off..dx_n_g_off + dx_stride_g],
15561 c_in_per_g,
15562 h,
15563 w_in,
15564 h_out,
15565 w_out,
15566 kh,
15567 kw,
15568 sh,
15569 sw,
15570 ph,
15571 pw,
15572 dh,
15573 dw,
15574 );
15575 }
15576 }
15577 }
15578 }
15579 }
15580
15581 Thunk::Conv2dBackwardWeight {
15582 x,
15583 dy,
15584 dw,
15585 n,
15586 c_in,
15587 h,
15588 w,
15589 c_out,
15590 h_out,
15591 w_out,
15592 kh,
15593 kw,
15594 sh,
15595 sw,
15596 ph,
15597 pw,
15598 dh,
15599 dw_dil,
15600 groups,
15601 } => {
15602 let n = *n as usize;
15603 let c_in = *c_in as usize;
15604 let h = *h as usize;
15605 let w = *w as usize;
15606 let c_out = *c_out as usize;
15617 let h_out = *h_out as usize;
15618 let w_out = *w_out as usize;
15619 let kh = *kh as usize;
15620 let kw = *kw as usize;
15621 let sh = *sh as usize;
15622 let sw = *sw as usize;
15623 let ph = *ph as usize;
15624 let pw = *pw as usize;
15625 let dh = *dh as usize;
15626 let dw_dil = *dw_dil as usize;
15627 let groups = *groups as usize;
15628 let c_in_per_g = c_in / groups;
15629 let c_out_per_g = c_out / groups;
15630
15631 let m_dim = c_out_per_g;
15632 let n_dim = c_in_per_g * kh * kw;
15633 let k_dim = h_out * w_out;
15634
15635 let x_stride_n = c_in * h * w;
15636 let x_stride_g = c_in_per_g * h * w;
15637 let dy_stride_n = c_out * h_out * w_out;
15638 let dy_stride_g = c_out_per_g * h_out * w_out;
15639 let dw_stride_g = c_out_per_g * c_in_per_g * kh * kw;
15640
15641 unsafe {
15642 let xs = sl(*x, base, n * c_in * h * w);
15643 let dys = sl(*dy, base, n * c_out * h_out * w_out);
15644 let dws = sl_mut(*dw, base, c_out * c_in_per_g * kh * kw);
15645 for v in dws.iter_mut() {
15646 *v = 0.0;
15647 }
15648
15649 if fast_conv_enabled() {
15655 let dw_len = dws.len();
15656 let dws_addr = dws.as_mut_ptr() as usize;
15657 let lock = std::sync::Mutex::new(());
15658 crate::pool::par_for(n, 1, &|off, cnt| {
15659 let mut col = vec![0f32; n_dim * k_dim];
15660 let mut local = vec![0f32; dw_len];
15661 for ni in off..off + cnt {
15662 for g in 0..groups {
15663 let x_n_g_off = ni * x_stride_n + g * x_stride_g;
15664 let dy_n_g_off = ni * dy_stride_n + g * dy_stride_g;
15665 let dw_g_off = g * dw_stride_g;
15666 crate::im2col::im2col_rows_layout(
15671 &xs[x_n_g_off..x_n_g_off + x_stride_g],
15672 &mut col,
15673 1,
15674 c_in_per_g,
15675 h,
15676 w,
15677 h_out,
15678 w_out,
15679 kh,
15680 kw,
15681 sh,
15682 sw,
15683 ph,
15684 pw,
15685 dh,
15686 dw_dil,
15687 );
15688 crate::blas::sgemm_general(
15689 dys.as_ptr().add(dy_n_g_off),
15690 col.as_ptr(),
15691 local.as_mut_ptr().add(dw_g_off),
15692 m_dim,
15693 n_dim,
15694 k_dim,
15695 1.0,
15696 1.0,
15697 k_dim,
15698 n_dim,
15699 n_dim,
15700 false,
15701 false,
15702 );
15703 }
15704 }
15705 let _guard = lock.lock().unwrap();
15706 let dws = std::slice::from_raw_parts_mut(dws_addr as *mut f32, dw_len);
15707 for (d, l) in dws.iter_mut().zip(local.iter()) {
15708 *d += *l;
15709 }
15710 });
15711 } else {
15712 let mut col = vec![0f32; n_dim * k_dim];
15713 for ni in 0..n {
15714 for g in 0..groups {
15715 let x_n_g_off = ni * x_stride_n + g * x_stride_g;
15716 im2col(
15717 &xs[x_n_g_off..x_n_g_off + x_stride_g],
15718 &mut col,
15719 c_in_per_g,
15720 h,
15721 w,
15722 h_out,
15723 w_out,
15724 kh,
15725 kw,
15726 sh,
15727 sw,
15728 ph,
15729 pw,
15730 dh,
15731 dw_dil,
15732 );
15733
15734 let dy_n_g_off = ni * dy_stride_n + g * dy_stride_g;
15735 let dw_g_off = g * dw_stride_g;
15736
15737 crate::blas::sgemm_general(
15745 dys.as_ptr().add(dy_n_g_off),
15746 col.as_ptr(),
15747 dws.as_mut_ptr().add(dw_g_off),
15748 m_dim,
15749 n_dim,
15750 k_dim,
15751 1.0,
15752 1.0,
15753 k_dim,
15754 k_dim,
15755 n_dim,
15756 false,
15757 true,
15758 );
15759 }
15760 }
15761 }
15762 }
15763 }
15764
15765 Thunk::Im2Col {
15766 x,
15767 col,
15768 n,
15769 c_in,
15770 h,
15771 w,
15772 h_out,
15773 w_out,
15774 kh,
15775 kw,
15776 sh,
15777 sw,
15778 ph,
15779 pw,
15780 dh,
15781 dw_dil,
15782 } => {
15783 let c_in = *c_in as usize;
15784 let h = *h as usize;
15785 let w = *w as usize;
15786 let h_out = *h_out as usize;
15787 let w_out = *w_out as usize;
15788 let kh = *kh as usize;
15789 let kw = *kw as usize;
15790 let sh = *sh as usize;
15791 let sw = *sw as usize;
15792 let ph = *ph as usize;
15793 let pw = *pw as usize;
15794 let dh = *dh as usize;
15795 let dw_dil = *dw_dil as usize;
15796 let per_batch = c_in * h * w;
15797 unsafe {
15798 let n_eff = if *n == 0 { 0usize } else { *n as usize };
15799 let x_floats = if n_eff == 0 {
15800 per_batch.max(1)
15801 } else {
15802 n_eff * per_batch
15803 };
15804 let xs = sl(*x, base, x_floats);
15805 let n = if *n == 0 {
15806 xs.len() / per_batch.max(1)
15807 } else {
15808 n_eff
15809 };
15810 let m = n * h_out * w_out;
15811 let k = c_in * kh * kw;
15812 let cols = sl_mut(*col, base, m * k);
15813 crate::im2col::im2col_rows_layout(
15814 xs, cols, n, c_in, h, w, h_out, w_out, kh, kw, sh, sw, ph, pw, dh, dw_dil,
15815 );
15816 }
15817 }
15818
15819 Thunk::SoftmaxCrossEntropyDense {
15820 logits,
15821 targets,
15822 dst,
15823 n,
15824 c,
15825 } => {
15826 let n = *n as usize;
15827 let c = *c as usize;
15828 unsafe {
15829 let lg = sl(*logits, base, n * c);
15830 let tg = sl(*targets, base, n * c);
15831 let out = sl_mut(*dst, base, n);
15832 for ni in 0..n {
15833 let row = &lg[ni * c..(ni + 1) * c];
15834 let trow = &tg[ni * c..(ni + 1) * c];
15835 let mut m = f32::NEG_INFINITY;
15837 for &v in row {
15838 if v > m {
15839 m = v;
15840 }
15841 }
15842 let mut sum = 0f32;
15843 for &v in row {
15844 sum += (v - m).exp();
15845 }
15846 let lse = m + sum.ln();
15847 let mut dot = 0f32;
15849 for k in 0..c {
15850 dot += trow[k] * row[k];
15851 }
15852 out[ni] = lse - dot;
15853 }
15854 }
15855 }
15856
15857 Thunk::SoftmaxCrossEntropy {
15858 logits,
15859 labels,
15860 dst,
15861 n,
15862 c,
15863 } => {
15864 let n = *n as usize;
15865 let c = *c as usize;
15866 unsafe {
15867 let lg = sl(*logits, base, n * c);
15868 let lb = sl(*labels, base, n);
15869 let out = sl_mut(*dst, base, n);
15870 for ni in 0..n {
15871 let row = &lg[ni * c..(ni + 1) * c];
15872 let mut m = f32::NEG_INFINITY;
15874 for &v in row {
15875 if v > m {
15876 m = v;
15877 }
15878 }
15879 let mut sum = 0f32;
15880 for &v in row {
15881 sum += (v - m).exp();
15882 }
15883 let lse = m + sum.ln();
15884 let label_idx = lb[ni] as usize;
15885 out[ni] = lse - row[label_idx];
15887 }
15888 }
15889 }
15890
15891 Thunk::SoftmaxCrossEntropyBackward {
15892 logits,
15893 labels,
15894 d_loss,
15895 dlogits,
15896 n,
15897 c,
15898 } => {
15899 let n = *n as usize;
15900 let c = *c as usize;
15901 unsafe {
15902 let lg = sl(*logits, base, n * c);
15903 let lb = sl(*labels, base, n);
15904 let dl = sl(*d_loss, base, n);
15905 let out = sl_mut(*dlogits, base, n * c);
15906 for ni in 0..n {
15907 let row = &lg[ni * c..(ni + 1) * c];
15908 let label_idx = lb[ni] as usize;
15909 let scale = dl[ni];
15910 let mut m = f32::NEG_INFINITY;
15911 for &v in row {
15912 if v > m {
15913 m = v;
15914 }
15915 }
15916 let mut sum = 0f32;
15917 for &v in row {
15918 sum += (v - m).exp();
15919 }
15920 let inv_sum = 1.0 / sum;
15921 let dst_row = &mut out[ni * c..(ni + 1) * c];
15922 for k in 0..c {
15923 let p = (row[k] - m).exp() * inv_sum;
15924 let one_hot = if k == label_idx { 1.0 } else { 0.0 };
15925 dst_row[k] = (p - one_hot) * scale;
15926 }
15927 }
15928 }
15929 }
15930
15931 Thunk::GatherAxis {
15932 table,
15933 idx,
15934 dst,
15935 outer,
15936 axis_dim,
15937 num_idx,
15938 trailing,
15939 idx_i64,
15940 table_bytes,
15941 } => {
15942 let outer = *outer as usize;
15943 let axis_dim = *axis_dim as usize;
15944 let num_idx = *num_idx as usize;
15945 let trailing = *trailing as usize;
15946 unsafe {
15947 if *table_bytes == 8 {
15948 let tab = sl_i64(*table, base, outer * axis_dim * trailing);
15949 let out = sl_mut_i64(*dst, base, outer * num_idx * trailing);
15950 for o in 0..outer {
15951 let tab_outer = o * axis_dim * trailing;
15952 let out_outer = o * num_idx * trailing;
15953 if *idx_i64 != 0 {
15954 let ids = sl_i64(*idx, base, num_idx);
15955 for k in 0..num_idx {
15956 let row = ids[k].max(0) as usize;
15957 if row < axis_dim {
15958 let tab_row = tab_outer + row * trailing;
15959 let out_row = out_outer + k * trailing;
15960 out[out_row..out_row + trailing]
15961 .copy_from_slice(&tab[tab_row..tab_row + trailing]);
15962 }
15963 }
15964 } else {
15965 let ids = sl(*idx, base, num_idx);
15966 for k in 0..num_idx {
15967 let row = ids[k] as usize;
15968 if row < axis_dim {
15969 let tab_row = tab_outer + row * trailing;
15970 let out_row = out_outer + k * trailing;
15971 out[out_row..out_row + trailing]
15972 .copy_from_slice(&tab[tab_row..tab_row + trailing]);
15973 }
15974 }
15975 }
15976 }
15977 } else {
15978 let tab = sl(*table, base, outer * axis_dim * trailing);
15979 let out = sl_mut(*dst, base, outer * num_idx * trailing);
15980 for o in 0..outer {
15981 let tab_outer = o * axis_dim * trailing;
15982 let out_outer = o * num_idx * trailing;
15983 if *idx_i64 != 0 {
15984 let ids = sl_i64(*idx, base, num_idx);
15985 for k in 0..num_idx {
15986 let row = ids[k].max(0) as usize;
15987 if row < axis_dim {
15988 let tab_row = tab_outer + row * trailing;
15989 let out_row = out_outer + k * trailing;
15990 out[out_row..out_row + trailing]
15991 .copy_from_slice(&tab[tab_row..tab_row + trailing]);
15992 }
15993 }
15994 } else {
15995 let ids = sl(*idx, base, num_idx);
15996 for k in 0..num_idx {
15997 let row = ids[k] as usize;
15998 if row < axis_dim {
15999 let tab_row = tab_outer + row * trailing;
16000 let out_row = out_outer + k * trailing;
16001 out[out_row..out_row + trailing]
16002 .copy_from_slice(&tab[tab_row..tab_row + trailing]);
16003 }
16004 }
16005 }
16006 }
16007 }
16008 }
16009 }
16010
16011 Thunk::Transpose {
16012 src,
16013 dst,
16014 in_total,
16015 out_dims,
16016 in_strides,
16017 elem_bytes,
16018 } => {
16019 let rank = out_dims.len();
16024 let total: usize = out_dims.iter().map(|&d| d as usize).product();
16025 let in_total = *in_total as usize;
16026 unsafe {
16027 if *elem_bytes == 1 {
16028 let inp = arena_buf[*src..*src + in_total].to_vec();
16033 let out = &mut arena_buf[*dst..*dst + total];
16034 let mut idx = vec![0usize; rank];
16035 for o in 0..total {
16036 let mut src_idx = 0usize;
16037 for d in 0..rank {
16038 src_idx += idx[d] * in_strides[d] as usize;
16039 }
16040 out[o] = inp[broadcast_src_index(src_idx, in_total)];
16041 for d in (0..rank).rev() {
16042 idx[d] += 1;
16043 if idx[d] < out_dims[d] as usize {
16044 break;
16045 }
16046 idx[d] = 0;
16047 }
16048 }
16049 } else if *elem_bytes == 8 {
16050 let inp = sl_i64(*src, base, in_total);
16051 let out = sl_mut_i64(*dst, base, total);
16052 let mut idx = vec![0usize; rank];
16053 for o in 0..total {
16054 let mut src_idx = 0usize;
16055 for d in 0..rank {
16056 src_idx += idx[d] * in_strides[d] as usize;
16057 }
16058 out[o] = inp[broadcast_src_index(src_idx, in_total)];
16059 for d in (0..rank).rev() {
16060 idx[d] += 1;
16061 if idx[d] < out_dims[d] as usize {
16062 break;
16063 }
16064 idx[d] = 0;
16065 }
16066 }
16067 } else {
16068 let inp = sl(*src, base, in_total);
16069 let out = sl_mut(*dst, base, total);
16070 if rank == 4
16071 && in_strides[0] == 0
16072 && in_strides[2] == 0
16073 && in_strides[3] == 0
16074 && in_strides[1] != 0
16075 {
16076 let d1 = out_dims[1] as usize;
16082 let sc = in_strides[1] as usize;
16083 let plane = (out_dims[2] as usize) * (out_dims[3] as usize);
16084 let nc_total = (out_dims[0] as usize) * d1;
16085 let out_addr = out.as_mut_ptr() as usize;
16086 let fill = |nc0: usize, nc1: usize| {
16087 let op = out_addr as *mut f32;
16088 for nc in nc0..nc1 {
16089 let v = inp[(nc % d1) * sc];
16090 let base_off = nc * plane;
16091 for k in 0..plane {
16092 *op.add(base_off + k) = v;
16093 }
16094 }
16095 };
16096 if fast_conv_enabled() && crate::pool::should_parallelize(total) {
16097 crate::pool::par_for(
16098 nc_total,
16099 crate::pool::outer_chunk(nc_total),
16100 &|off, cnt| fill(off, off + cnt),
16101 );
16102 } else {
16103 fill(0, nc_total);
16104 }
16105 } else if rank == 2 && in_strides[0] != 0 && in_strides[1] != 0 {
16106 let d0 = out_dims[0] as usize;
16111 let d1 = out_dims[1] as usize;
16112 let s0 = in_strides[0] as usize;
16113 let s1 = in_strides[1] as usize;
16114 let out_addr = out.as_mut_ptr() as usize;
16115 let tile = |i0: usize, i1: usize| {
16116 let op = out_addr as *mut f32;
16117 const T: usize = 32;
16118 let mut j0 = 0;
16119 while j0 < d1 {
16120 let j1 = (j0 + T).min(d1);
16121 for i in i0..i1 {
16122 let inb = i * s0;
16123 let outb = i * d1;
16124 for j in j0..j1 {
16125 *op.add(outb + j) = inp[inb + j * s1];
16126 }
16127 }
16128 j0 = j1;
16129 }
16130 };
16131 if fast_conv_enabled() && crate::pool::should_parallelize(total) {
16132 crate::pool::par_for(
16133 d0,
16134 crate::pool::outer_chunk(d0),
16135 &|off, cnt| tile(off, off + cnt),
16136 );
16137 } else {
16138 tile(0, d0);
16139 }
16140 } else if fast_conv_enabled() && crate::pool::should_parallelize(total) {
16141 let out_addr = out.as_mut_ptr() as usize;
16145 crate::pool::par_for(
16146 total,
16147 crate::pool::chunk_floor(total),
16148 &|off, cnt| {
16149 let mut idx = vec![0usize; rank];
16150 let mut rem = off;
16151 for d in (0..rank).rev() {
16152 let dim = out_dims[d] as usize;
16153 idx[d] = rem % dim;
16154 rem /= dim;
16155 }
16156 for o in off..off + cnt {
16157 let mut src_idx = 0usize;
16158 for d in 0..rank {
16159 src_idx += idx[d] * in_strides[d] as usize;
16160 }
16161 let v = inp[broadcast_src_index(src_idx, in_total)];
16162 *((out_addr as *mut f32).add(o)) = v;
16163 for d in (0..rank).rev() {
16164 idx[d] += 1;
16165 if idx[d] < out_dims[d] as usize {
16166 break;
16167 }
16168 idx[d] = 0;
16169 }
16170 }
16171 },
16172 );
16173 } else {
16174 let mut idx = vec![0usize; rank];
16175 for o in 0..total {
16176 let mut src_idx = 0usize;
16177 for d in 0..rank {
16178 src_idx += idx[d] * in_strides[d] as usize;
16179 }
16180 out[o] = inp[broadcast_src_index(src_idx, in_total)];
16181 for d in (0..rank).rev() {
16182 idx[d] += 1;
16183 if idx[d] < out_dims[d] as usize {
16184 break;
16185 }
16186 idx[d] = 0;
16187 }
16188 }
16189 }
16190 }
16191 }
16192 }
16193
16194 Thunk::CustomOp {
16200 kernel,
16201 inputs,
16202 output,
16203 attrs,
16204 } => {
16205 let (out_off, out_len, out_shape) = output;
16206 unsafe {
16207 dispatch_custom_op(
16208 &**kernel, inputs, *out_off, *out_len, out_shape, attrs, base,
16209 );
16210 }
16211 }
16212
16213 Thunk::Reverse {
16214 src,
16215 dst,
16216 dims,
16217 rev_mask,
16218 elem_bytes,
16219 } => {
16220 let eb = *elem_bytes as usize;
16221 let rank = dims.len();
16222 let total: usize = dims.iter().map(|&d| d as usize).product::<usize>().max(1);
16223 let mut strides = vec![1usize; rank];
16224 for i in (0..rank.saturating_sub(1)).rev() {
16225 strides[i] = strides[i + 1] * dims[i + 1] as usize;
16226 }
16227 unsafe {
16228 let src_base = base.add(*src);
16229 let dst_base = base.add(*dst);
16230 for o in 0..total {
16231 let mut rem = o;
16232 let mut in_flat = 0usize;
16233 for ax in 0..rank {
16234 let idx = rem / strides[ax];
16235 rem %= strides[ax];
16236 let in_idx = if rev_mask[ax] {
16237 dims[ax] as usize - 1 - idx
16238 } else {
16239 idx
16240 };
16241 in_flat += in_idx * strides[ax];
16242 }
16243 std::ptr::copy_nonoverlapping(
16244 src_base.add(in_flat * eb),
16245 dst_base.add(o * eb),
16246 eb,
16247 );
16248 }
16249 }
16250 }
16251 }
16252 if trace_done {
16253 eprintln!("[thunk {i} done]");
16254 }
16255 }
16256}
16257
16258#[allow(clippy::too_many_arguments)]
16273unsafe fn griewank_process_segment(
16274 t_lo: usize,
16275 t_hi: usize,
16276 anchor_carry: &[u8],
16277 cb: usize,
16278 fwd_sched: &ThunkSchedule,
16279 fwd_init: &[u8],
16280 fwd_carry_in_off: usize,
16281 fwd_output_off: usize,
16282 fwd_x_offs: &[usize],
16283 base: *mut u8,
16284 outer_xs_offs: &[(usize, u32)],
16285 fwd_buf: &mut Vec<u8>,
16286 leaf_threshold: usize,
16287 process_iter: &mut dyn FnMut(usize, &[u8]),
16288) {
16289 unsafe {
16290 let size = t_hi - t_lo + 1;
16291 if size == 1 {
16292 process_iter(t_lo, anchor_carry);
16293 return;
16294 }
16295 if size <= leaf_threshold {
16296 let mut cache: Vec<u8> = Vec::with_capacity(size * cb);
16298 cache.extend_from_slice(anchor_carry);
16299 fwd_buf.copy_from_slice(fwd_init);
16300 std::ptr::copy_nonoverlapping(
16301 anchor_carry.as_ptr(),
16302 fwd_buf.as_mut_ptr().add(fwd_carry_in_off),
16303 cb,
16304 );
16305 for i in 1..size {
16306 let cur_iter = t_lo + i - 1;
16307 for (idx, fb_x_off) in fwd_x_offs.iter().enumerate() {
16308 let (outer_xs_off, x_psb) = outer_xs_offs[idx];
16309 let xb = x_psb as usize;
16310 std::ptr::copy_nonoverlapping(
16311 base.add(outer_xs_off + cur_iter * xb),
16312 fwd_buf.as_mut_ptr().add(*fb_x_off),
16313 xb,
16314 );
16315 }
16316 execute_thunks(fwd_sched, fwd_buf);
16317 if fwd_output_off != fwd_carry_in_off {
16318 fwd_buf.copy_within(fwd_output_off..fwd_output_off + cb, fwd_carry_in_off);
16319 }
16320 cache.extend_from_slice(&fwd_buf[fwd_carry_in_off..fwd_carry_in_off + cb]);
16321 }
16322 for t in (t_lo..=t_hi).rev() {
16324 let idx = t - t_lo;
16325 let carry = &cache[idx * cb..(idx + 1) * cb];
16326 process_iter(t, carry);
16327 }
16328 return;
16329 }
16330
16331 let mid = t_lo + size / 2;
16335 fwd_buf.copy_from_slice(fwd_init);
16336 std::ptr::copy_nonoverlapping(
16337 anchor_carry.as_ptr(),
16338 fwd_buf.as_mut_ptr().add(fwd_carry_in_off),
16339 cb,
16340 );
16341 for cur_iter in t_lo..mid {
16342 for (idx, fb_x_off) in fwd_x_offs.iter().enumerate() {
16343 let (outer_xs_off, x_psb) = outer_xs_offs[idx];
16344 let xb = x_psb as usize;
16345 std::ptr::copy_nonoverlapping(
16346 base.add(outer_xs_off + cur_iter * xb),
16347 fwd_buf.as_mut_ptr().add(*fb_x_off),
16348 xb,
16349 );
16350 }
16351 execute_thunks(fwd_sched, fwd_buf);
16352 if fwd_output_off != fwd_carry_in_off {
16353 fwd_buf.copy_within(fwd_output_off..fwd_output_off + cb, fwd_carry_in_off);
16354 }
16355 }
16356 let mid_carry: Vec<u8> = fwd_buf[fwd_carry_in_off..fwd_carry_in_off + cb].to_vec();
16357
16358 griewank_process_segment(
16362 mid,
16363 t_hi,
16364 &mid_carry,
16365 cb,
16366 fwd_sched,
16367 fwd_init,
16368 fwd_carry_in_off,
16369 fwd_output_off,
16370 fwd_x_offs,
16371 base,
16372 outer_xs_offs,
16373 fwd_buf,
16374 leaf_threshold,
16375 process_iter,
16376 );
16377 griewank_process_segment(
16379 t_lo,
16380 mid - 1,
16381 anchor_carry,
16382 cb,
16383 fwd_sched,
16384 fwd_init,
16385 fwd_carry_in_off,
16386 fwd_output_off,
16387 fwd_x_offs,
16388 base,
16389 outer_xs_offs,
16390 fwd_buf,
16391 leaf_threshold,
16392 process_iter,
16393 );
16394 }
16395}
16396
16397pub unsafe fn execute_fft1d_f64(
16414 src: usize,
16415 dst: usize,
16416 outer: usize,
16417 n_complex: usize,
16418 inverse: bool,
16419 norm_tag: u32,
16420 base: *mut u8,
16421) {
16422 let row_elems = 2 * n_complex;
16423 let mut re = vec![0f64; n_complex];
16424 let mut im = vec![0f64; n_complex];
16425 let norm = rlx_ir::fft::FftNorm::from_tag(norm_tag);
16426 let scale = norm.output_scale(n_complex, inverse);
16427 let mut scratch = if n_complex.is_power_of_two() || n_complex <= 16 {
16430 BluesteinScratchF64::empty()
16431 } else {
16432 BluesteinScratchF64::build(n_complex, inverse)
16433 };
16434 for o in 0..outer {
16435 let row_offset = src + o * row_elems * std::mem::size_of::<f64>();
16436 let s = unsafe { sl_f64(row_offset, base, row_elems) };
16437 re.copy_from_slice(&s[..n_complex]);
16438 im.copy_from_slice(&s[n_complex..]);
16439 if n_complex.is_power_of_two() {
16440 fft_radix2_inplace_f64(&mut re, &mut im, inverse);
16441 } else if n_complex <= 16 {
16442 fft_naive_inplace_f64(&mut re, &mut im, inverse);
16443 } else {
16444 fft_bluestein_inplace_f64(&mut re, &mut im, inverse, &mut scratch);
16445 }
16446 if scale != 1.0 {
16447 re.iter_mut().for_each(|v| *v *= scale);
16448 im.iter_mut().for_each(|v| *v *= scale);
16449 }
16450 let dst_offset = dst + o * row_elems * std::mem::size_of::<f64>();
16451 let d = unsafe { sl_mut_f64(dst_offset, base, row_elems) };
16452 d[..n_complex].copy_from_slice(&re);
16453 d[n_complex..].copy_from_slice(&im);
16454 }
16455}
16456
16457unsafe fn cgemm_c64(
16467 a_off: usize,
16468 b_off: usize,
16469 c_off: usize,
16470 m: usize,
16471 k: usize,
16472 n: usize,
16473 base: *mut u8,
16474) {
16475 let bptr = base as usize;
16476 unsafe {
16477 let a = std::slice::from_raw_parts((bptr + a_off) as *const f32, 2 * m * k);
16478 let b = std::slice::from_raw_parts((bptr + b_off) as *const f32, 2 * k * n);
16479 let c_base = bptr + c_off;
16480 crate::pool::par_range(m, |i| {
16481 let crow = std::slice::from_raw_parts_mut((c_base + i * n * 8) as *mut f32, 2 * n);
16482 for j in 0..n {
16483 let mut re = 0f32;
16484 let mut im = 0f32;
16485 for l in 0..k {
16486 let ar = a[2 * (i * k + l)];
16487 let ai = a[2 * (i * k + l) + 1];
16488 let br = b[2 * (l * n + j)];
16489 let bi = b[2 * (l * n + j) + 1];
16490 re += ar * br - ai * bi;
16491 im += ar * bi + ai * br;
16492 }
16493 crow[2 * j] = re;
16494 crow[2 * j + 1] = im;
16495 }
16496 });
16497 }
16498}
16499
16500#[allow(clippy::too_many_arguments)]
16508pub unsafe fn execute_lstm_f32(
16509 x: usize,
16510 w_ih: usize,
16511 w_hh: usize,
16512 bias: usize,
16513 h0: usize,
16514 c0: usize,
16515 dst: usize,
16516 batch: usize,
16517 seq: usize,
16518 input_size: usize,
16519 hidden: usize,
16520 num_layers: usize,
16521 bidirectional: bool,
16522 carry: bool,
16523 base: *mut u8,
16524) {
16525 #[inline]
16526 fn sigmoid(z: f32) -> f32 {
16527 1.0 / (1.0 + (-z).exp())
16528 }
16529
16530 let bptr = base as usize;
16531 let four_h = 4 * hidden;
16532 let dirs = if bidirectional { 2 } else { 1 };
16533
16534 unsafe {
16535 let f32s = |off: usize, n: usize| -> &[f32] {
16536 std::slice::from_raw_parts((bptr + off) as *const f32, n)
16537 };
16538
16539 let mut layer_in: Vec<f32> = f32s(x, batch * seq * input_size).to_vec();
16541 let mut in_l = input_size;
16542 let mut wih_cursor = 0usize;
16545
16546 for l in 0..num_layers {
16547 let out_width = dirs * hidden;
16548 let mut layer_out = vec![0f32; batch * seq * out_width];
16549 let lo_ptr = layer_out.as_mut_ptr() as usize;
16550 let li_ref: &[f32] = &layer_in;
16551 let wih_block = four_h * in_l;
16552
16553 for dir in 0..dirs {
16554 let ld = l * dirs + dir;
16555 let wih = f32s((w_ih / 4 + wih_cursor + dir * wih_block) * 4, wih_block);
16556 let whh = f32s(w_hh + ld * four_h * hidden * 4, four_h * hidden);
16557 let bs = f32s(bias + ld * four_h * 4, four_h);
16558 let h0p = bptr + h0 + ld * batch * hidden * 4;
16559 let c0p = bptr + c0 + ld * batch * hidden * 4;
16560
16561 crate::pool::par_range(batch, |b| {
16562 let lo = lo_ptr as *mut f32;
16563 let mut h = vec![0f32; hidden];
16564 let mut c = vec![0f32; hidden];
16565 if carry {
16566 let hin = std::slice::from_raw_parts(
16567 (h0p + b * hidden * 4) as *const f32,
16568 hidden,
16569 );
16570 let cin = std::slice::from_raw_parts(
16571 (c0p + b * hidden * 4) as *const f32,
16572 hidden,
16573 );
16574 h.copy_from_slice(hin);
16575 c.copy_from_slice(cin);
16576 }
16577 let mut z = vec![0f32; four_h];
16578 for step in 0..seq {
16579 let t = if dir == 0 { step } else { seq - 1 - step };
16580 let x_t = &li_ref[(b * seq + t) * in_l..(b * seq + t + 1) * in_l];
16581 for r in 0..four_h {
16582 let wr = &wih[r * in_l..(r + 1) * in_l];
16583 let mut acc = bs[r];
16584 for j in 0..in_l {
16585 acc += wr[j] * x_t[j];
16586 }
16587 let hr = &whh[r * hidden..(r + 1) * hidden];
16588 for (j, &hj) in h.iter().enumerate() {
16589 acc += hr[j] * hj;
16590 }
16591 z[r] = acc;
16592 }
16593 for k in 0..hidden {
16594 let i_g = sigmoid(z[k]);
16595 let f_g = sigmoid(z[hidden + k]);
16596 let g_g = z[2 * hidden + k].tanh();
16597 let o_g = sigmoid(z[3 * hidden + k]);
16598 let c_new = f_g * c[k] + i_g * g_g;
16599 c[k] = c_new;
16600 let h_new = o_g * c_new.tanh();
16601 h[k] = h_new;
16602 *lo.add((b * seq + t) * out_width + dir * hidden + k) = h_new;
16605 }
16606 }
16607 if carry {
16608 let hout = std::slice::from_raw_parts_mut(
16609 (h0p + b * hidden * 4) as *mut f32,
16610 hidden,
16611 );
16612 let cout = std::slice::from_raw_parts_mut(
16613 (c0p + b * hidden * 4) as *mut f32,
16614 hidden,
16615 );
16616 hout.copy_from_slice(&h);
16617 cout.copy_from_slice(&c);
16618 }
16619 });
16620 }
16621
16622 wih_cursor += dirs * wih_block;
16623 layer_in = layer_out;
16624 in_l = out_width;
16625 }
16626
16627 let dst_slice = std::slice::from_raw_parts_mut((bptr + dst) as *mut f32, layer_in.len());
16629 dst_slice.copy_from_slice(&layer_in);
16630 }
16631}
16632
16633#[allow(clippy::too_many_arguments)]
16639pub unsafe fn execute_gru_f32(
16640 x: usize,
16641 w_ih: usize,
16642 w_hh: usize,
16643 b_ih: usize,
16644 b_hh: usize,
16645 h0: usize,
16646 dst: usize,
16647 batch: usize,
16648 seq: usize,
16649 input_size: usize,
16650 hidden: usize,
16651 num_layers: usize,
16652 bidirectional: bool,
16653 carry: bool,
16654 base: *mut u8,
16655) {
16656 #[inline]
16657 fn sigmoid(z: f32) -> f32 {
16658 1.0 / (1.0 + (-z).exp())
16659 }
16660
16661 let bptr = base as usize;
16662 let three_h = 3 * hidden;
16663 let dirs = if bidirectional { 2 } else { 1 };
16664
16665 unsafe {
16666 let f32s = |off: usize, n: usize| -> &[f32] {
16667 std::slice::from_raw_parts((bptr + off) as *const f32, n)
16668 };
16669
16670 let mut layer_in: Vec<f32> = f32s(x, batch * seq * input_size).to_vec();
16671 let mut in_l = input_size;
16672 let mut wih_cursor = 0usize;
16673
16674 for l in 0..num_layers {
16675 let out_width = dirs * hidden;
16676 let mut layer_out = vec![0f32; batch * seq * out_width];
16677 let lo_ptr = layer_out.as_mut_ptr() as usize;
16678 let li_ref: &[f32] = &layer_in;
16679 let wih_block = three_h * in_l;
16680
16681 for dir in 0..dirs {
16682 let ld = l * dirs + dir;
16683 let wih = f32s((w_ih / 4 + wih_cursor + dir * wih_block) * 4, wih_block);
16684 let whh = f32s(w_hh + ld * three_h * hidden * 4, three_h * hidden);
16685 let bih = f32s(b_ih + ld * three_h * 4, three_h);
16686 let bhh = f32s(b_hh + ld * three_h * 4, three_h);
16687 let h0p = bptr + h0 + ld * batch * hidden * 4;
16688
16689 crate::pool::par_range(batch, |b| {
16690 let lo = lo_ptr as *mut f32;
16691 let mut h = vec![0f32; hidden];
16692 if carry {
16693 let hin = std::slice::from_raw_parts(
16694 (h0p + b * hidden * 4) as *const f32,
16695 hidden,
16696 );
16697 h.copy_from_slice(hin);
16698 }
16699 let mut xi = vec![0f32; three_h]; let mut hi = vec![0f32; three_h]; for step in 0..seq {
16702 let t = if dir == 0 { step } else { seq - 1 - step };
16703 let x_t = &li_ref[(b * seq + t) * in_l..(b * seq + t + 1) * in_l];
16704 for r in 0..three_h {
16705 let wr = &wih[r * in_l..(r + 1) * in_l];
16706 let mut a = bih[r];
16707 for j in 0..in_l {
16708 a += wr[j] * x_t[j];
16709 }
16710 xi[r] = a;
16711 let hr = &whh[r * hidden..(r + 1) * hidden];
16712 let mut bb = bhh[r];
16713 for (j, &hj) in h.iter().enumerate() {
16714 bb += hr[j] * hj;
16715 }
16716 hi[r] = bb;
16717 }
16718 for k in 0..hidden {
16719 let rg = sigmoid(xi[k] + hi[k]);
16720 let zg = sigmoid(xi[hidden + k] + hi[hidden + k]);
16721 let ng = (xi[2 * hidden + k] + rg * hi[2 * hidden + k]).tanh();
16723 let h_new = (1.0 - zg) * ng + zg * h[k];
16724 h[k] = h_new;
16725 *lo.add((b * seq + t) * out_width + dir * hidden + k) = h_new;
16726 }
16727 }
16728 if carry {
16729 let hout = std::slice::from_raw_parts_mut(
16730 (h0p + b * hidden * 4) as *mut f32,
16731 hidden,
16732 );
16733 hout.copy_from_slice(&h);
16734 }
16735 });
16736 }
16737
16738 wih_cursor += dirs * wih_block;
16739 layer_in = layer_out;
16740 in_l = out_width;
16741 }
16742
16743 let dst_slice = std::slice::from_raw_parts_mut((bptr + dst) as *mut f32, layer_in.len());
16744 dst_slice.copy_from_slice(&layer_in);
16745 }
16746}
16747
16748#[allow(clippy::too_many_arguments)]
16753pub unsafe fn execute_rnn_f32(
16754 x: usize,
16755 w_ih: usize,
16756 w_hh: usize,
16757 bias: usize,
16758 h0: usize,
16759 dst: usize,
16760 batch: usize,
16761 seq: usize,
16762 input_size: usize,
16763 hidden: usize,
16764 num_layers: usize,
16765 bidirectional: bool,
16766 carry: bool,
16767 relu: bool,
16768 base: *mut u8,
16769) {
16770 let bptr = base as usize;
16771 let dirs = if bidirectional { 2 } else { 1 };
16772
16773 unsafe {
16774 let f32s = |off: usize, n: usize| -> &[f32] {
16775 std::slice::from_raw_parts((bptr + off) as *const f32, n)
16776 };
16777
16778 let mut layer_in: Vec<f32> = f32s(x, batch * seq * input_size).to_vec();
16779 let mut in_l = input_size;
16780 let mut wih_cursor = 0usize;
16781
16782 for l in 0..num_layers {
16783 let out_width = dirs * hidden;
16784 let mut layer_out = vec![0f32; batch * seq * out_width];
16785 let lo_ptr = layer_out.as_mut_ptr() as usize;
16786 let li_ref: &[f32] = &layer_in;
16787 let wih_block = hidden * in_l;
16788
16789 for dir in 0..dirs {
16790 let ld = l * dirs + dir;
16791 let wih = f32s((w_ih / 4 + wih_cursor + dir * wih_block) * 4, wih_block);
16792 let whh = f32s(w_hh + ld * hidden * hidden * 4, hidden * hidden);
16793 let bs = f32s(bias + ld * hidden * 4, hidden);
16794 let h0p = bptr + h0 + ld * batch * hidden * 4;
16795
16796 crate::pool::par_range(batch, |b| {
16797 let lo = lo_ptr as *mut f32;
16798 let mut h = vec![0f32; hidden];
16799 if carry {
16800 let hin = std::slice::from_raw_parts(
16801 (h0p + b * hidden * 4) as *const f32,
16802 hidden,
16803 );
16804 h.copy_from_slice(hin);
16805 }
16806 for step in 0..seq {
16807 let t = if dir == 0 { step } else { seq - 1 - step };
16808 let x_t = &li_ref[(b * seq + t) * in_l..(b * seq + t + 1) * in_l];
16809 let mut h_new = vec![0f32; hidden];
16810 for k in 0..hidden {
16811 let wr = &wih[k * in_l..(k + 1) * in_l];
16812 let mut acc = bs[k];
16813 for j in 0..in_l {
16814 acc += wr[j] * x_t[j];
16815 }
16816 let hr = &whh[k * hidden..(k + 1) * hidden];
16817 for (j, &hj) in h.iter().enumerate() {
16818 acc += hr[j] * hj;
16819 }
16820 h_new[k] = if relu { acc.max(0.0) } else { acc.tanh() };
16821 }
16822 for k in 0..hidden {
16823 h[k] = h_new[k];
16824 *lo.add((b * seq + t) * out_width + dir * hidden + k) = h_new[k];
16825 }
16826 }
16827 if carry {
16828 let hout = std::slice::from_raw_parts_mut(
16829 (h0p + b * hidden * 4) as *mut f32,
16830 hidden,
16831 );
16832 hout.copy_from_slice(&h);
16833 }
16834 });
16835 }
16836
16837 wih_cursor += dirs * wih_block;
16838 layer_in = layer_out;
16839 in_l = out_width;
16840 }
16841
16842 let dst_slice = std::slice::from_raw_parts_mut((bptr + dst) as *mut f32, layer_in.len());
16843 dst_slice.copy_from_slice(&layer_in);
16844 }
16845}
16846
16847pub unsafe fn execute_argreduce_f32(
16852 src: usize,
16853 dst: usize,
16854 outer: usize,
16855 reduced: usize,
16856 inner: usize,
16857 is_max: bool,
16858 base: *mut u8,
16859) {
16860 let bptr = base as usize;
16861 unsafe {
16862 let inp = std::slice::from_raw_parts((bptr + src) as *const f32, outer * reduced * inner);
16863 let out = std::slice::from_raw_parts_mut((bptr + dst) as *mut f32, outer * inner);
16864 for o in 0..outer {
16865 for i in 0..inner {
16866 let mut best = inp[o * reduced * inner + i];
16867 let mut best_idx = 0usize;
16868 for r in 1..reduced {
16869 let v = inp[o * reduced * inner + r * inner + i];
16870 let better = if is_max { v > best } else { v < best };
16871 if better {
16872 best = v;
16873 best_idx = r;
16874 }
16875 }
16876 out[o * inner + i] = best_idx as f32;
16877 }
16878 }
16879 }
16880}
16881
16882#[allow(clippy::too_many_arguments)]
16887pub unsafe fn execute_mamba2_f32(
16888 x: usize,
16889 dt: usize,
16890 a: usize,
16891 b: usize,
16892 c: usize,
16893 dst: usize,
16894 batch: usize,
16895 seq: usize,
16896 heads: usize,
16897 head_dim: usize,
16898 state_size: usize,
16899 base: *mut u8,
16900) {
16901 let (bn, s, h, p, n) = (batch, seq, heads, head_dim, state_size);
16902 let bptr = base as usize;
16903 unsafe {
16904 let f32s = |off: usize, len: usize| -> &[f32] {
16905 std::slice::from_raw_parts((bptr + off) as *const f32, len)
16906 };
16907 let xs = f32s(x, bn * s * h * p);
16908 let dts = f32s(dt, bn * s * h);
16909 let am = f32s(a, h);
16910 let bm = f32s(b, bn * s * h * n);
16911 let cm = f32s(c, bn * s * h * n);
16912 let out_ptr = bptr + dst;
16913
16914 crate::pool::par_range(bn * h, |bh| {
16916 let bi = bh / h;
16917 let hi = bh % h;
16918 let out = out_ptr as *mut f32;
16919 let mut state = vec![0f32; p * n];
16920 for t in 0..s {
16921 let dt_t = dts[(bi * s + t) * h + hi];
16922 let da = (dt_t * am[hi]).exp();
16923 let x_off = ((bi * s + t) * h + hi) * p;
16924 let bc_off = ((bi * s + t) * h + hi) * n;
16925 for pi in 0..p {
16926 let dtx = dt_t * xs[x_off + pi];
16927 for ni in 0..n {
16928 state[pi * n + ni] = da * state[pi * n + ni] + dtx * bm[bc_off + ni];
16929 }
16930 }
16931 for pi in 0..p {
16932 let mut acc = 0f32;
16933 for ni in 0..n {
16934 acc += state[pi * n + ni] * cm[bc_off + ni];
16935 }
16936 *out.add(x_off + pi) = acc;
16937 }
16938 }
16939 });
16940 }
16941}
16942
16943pub unsafe fn execute_reverse(
16949 src: usize,
16950 dst: usize,
16951 dims: &[u32],
16952 rev_mask: &[bool],
16953 elem_bytes: usize,
16954 base: *mut u8,
16955) {
16956 let rank = dims.len();
16957 let total: usize = dims.iter().map(|&d| d as usize).product::<usize>().max(1);
16958 let mut strides = vec![1usize; rank];
16959 for i in (0..rank.saturating_sub(1)).rev() {
16960 strides[i] = strides[i + 1] * dims[i + 1] as usize;
16961 }
16962 unsafe {
16963 let src_base = base.add(src);
16964 let dst_base = base.add(dst);
16965 for o in 0..total {
16966 let mut rem = o;
16967 let mut in_flat = 0usize;
16968 for ax in 0..rank {
16969 let idx = rem / strides[ax];
16970 rem %= strides[ax];
16971 let in_idx = if rev_mask[ax] {
16972 dims[ax] as usize - 1 - idx
16973 } else {
16974 idx
16975 };
16976 in_flat += in_idx * strides[ax];
16977 }
16978 std::ptr::copy_nonoverlapping(
16979 src_base.add(in_flat * elem_bytes),
16980 dst_base.add(o * elem_bytes),
16981 elem_bytes,
16982 );
16983 }
16984 }
16985}
16986
16987pub unsafe fn execute_sample_f32(
16992 logits: usize,
16993 dst: usize,
16994 batch: usize,
16995 vocab: usize,
16996 top_k: usize,
16997 top_p: f32,
16998 temperature: f32,
16999 seed: u64,
17000 base: *mut u8,
17001) {
17002 let (b, v) = (batch, vocab);
17003 let k = top_k.min(v);
17004 unsafe {
17005 let lg = sl(logits, base, b * v);
17006 let out = sl_mut(dst, base, b);
17007 let mut rng = rlx_ir::Philox4x32::new(if seed == 0 { 0xDEADBEEF } else { seed });
17008 for bi in 0..b {
17009 let row = &lg[bi * v..(bi + 1) * v];
17010 out[bi] = sample_row(row, k, top_p, temperature, &mut rng) as f32;
17011 }
17012 }
17013}
17014
17015pub unsafe fn execute_selective_scan_f32(
17021 x: usize,
17022 delta: usize,
17023 a: usize,
17024 b: usize,
17025 c: usize,
17026 dst: usize,
17027 batch: usize,
17028 seq: usize,
17029 hidden: usize,
17030 state_size: usize,
17031 base: *mut u8,
17032) {
17033 let (bn, s, h, n) = (batch, seq, hidden, state_size);
17034 unsafe {
17035 let xs = sl(x, base, bn * s * h);
17036 let dt = sl(delta, base, bn * s * h);
17037 let am = sl(a, base, h * n);
17038 let bm = sl(b, base, bn * s * n);
17039 let cm = sl(c, base, bn * s * n);
17040 let out = sl_mut(dst, base, bn * s * h);
17041
17042 let mut state = vec![0f32; h * n];
17045 for bi in 0..bn {
17046 for v in state.iter_mut() {
17047 *v = 0.0;
17048 }
17049 for si in 0..s {
17050 let x_row = &xs[bi * s * h + si * h..bi * s * h + (si + 1) * h];
17051 let dt_row = &dt[bi * s * h + si * h..bi * s * h + (si + 1) * h];
17052 let b_row = &bm[bi * s * n + si * n..bi * s * n + (si + 1) * n];
17053 let c_row = &cm[bi * s * n + si * n..bi * s * n + (si + 1) * n];
17054 let out_row = &mut out[bi * s * h + si * h..bi * s * h + (si + 1) * h];
17055
17056 for ci in 0..h {
17057 let d = dt_row[ci];
17058 let xv = x_row[ci];
17059 let mut acc = 0f32;
17060 for ni in 0..n {
17061 let da = (d * am[ci * n + ni]).exp();
17063 state[ci * n + ni] = da * state[ci * n + ni] + d * b_row[ni] * xv;
17064 acc += c_row[ni] * state[ci * n + ni];
17065 }
17066 out_row[ci] = acc;
17067 }
17068 }
17069 }
17070 }
17071}
17072
17073pub unsafe fn execute_gated_delta_net_f32(
17074 q: usize,
17075 k: usize,
17076 v: usize,
17077 g: usize,
17078 beta: usize,
17079 state: usize,
17080 dst: usize,
17081 batch: usize,
17082 seq: usize,
17083 heads: usize,
17084 state_size: usize,
17085 base: *mut u8,
17086) {
17087 #[derive(Copy, Clone)]
17088 struct ArenaPtr(usize);
17089 unsafe impl Send for ArenaPtr {}
17090 unsafe impl Sync for ArenaPtr {}
17091 impl ArenaPtr {
17092 #[inline]
17093 fn get(self) -> *mut u8 {
17094 self.0 as *mut u8
17095 }
17096 }
17097
17098 unsafe {
17099 let arena = ArenaPtr(base as usize);
17100 let (b, s, h, n) = (batch, seq, heads, state_size);
17101 let scale = 1.0f32 / (n as f32).sqrt();
17102 let use_external = state != 0;
17103 let mut owned_state = vec![0f32; h * n * n];
17104
17105 crate::pool::num_threads();
17106
17107 assert!(
17108 n <= crate::gdn::GDN_MAX_STATE,
17109 "GatedDeltaNet state_size={n} exceeds stack scratch ({})",
17110 crate::gdn::GDN_MAX_STATE
17111 );
17112
17113 let qs = sl(q, arena.get(), b * s * h * n);
17114 let ks = sl(k, arena.get(), b * s * h * n);
17115 let vs = sl(v, arena.get(), b * s * h * n);
17116 let gs = sl(g, arena.get(), b * s * h);
17117 let betas = sl(beta, arena.get(), b * s * h);
17118 let _out = sl_mut(dst, arena.get(), b * s * h * n);
17119 let hs_n = h * n;
17120
17121 let run_head = |bi: usize, hi: usize, s_mat: &mut [f32], sk: &mut [f32]| {
17122 for ti in 0..s {
17123 let qkv_step = bi * s * hs_n + ti * hs_n + hi * n;
17124 let gb_step = bi * s * h + ti * h + hi;
17125 let out_row = sl_mut(dst + qkv_step * std::mem::size_of::<f32>(), arena.get(), n);
17126 crate::gdn::gdn_step_blas(
17127 s_mat,
17128 &qs[qkv_step..qkv_step + n],
17129 &ks[qkv_step..qkv_step + n],
17130 &vs[qkv_step..qkv_step + n],
17131 gs[gb_step],
17132 betas[gb_step],
17133 out_row,
17134 sk,
17135 n,
17136 scale,
17137 );
17138 }
17139 };
17140
17141 if !use_external && s > 1 {
17144 for bi in 0..b {
17145 crate::pool::par_range(h, |hi| {
17146 let mut sk_buf = [0f32; crate::gdn::GDN_MAX_STATE];
17147 let sk = &mut sk_buf[..n];
17148 let mut local_state =
17149 [0f32; crate::gdn::GDN_MAX_STATE * crate::gdn::GDN_MAX_STATE];
17150 let s_mat = &mut local_state[..n * n];
17151 s_mat.fill(0.0);
17152 run_head(bi, hi, s_mat, sk);
17153 });
17154 }
17155 return;
17156 }
17157
17158 if use_external {
17159 let state_bytes = state;
17160 crate::pool::par_range(b * h, |bhi| {
17161 let bi = bhi / h;
17162 let hi = bhi % h;
17163 let elem_off = bi * h * n * n + hi * n * n;
17164 let s_mat = sl_mut(
17165 state_bytes + elem_off * std::mem::size_of::<f32>(),
17166 arena.get(),
17167 n * n,
17168 );
17169 let mut sk_buf = [0f32; crate::gdn::GDN_MAX_STATE];
17170 run_head(bi, hi, s_mat, &mut sk_buf[..n]);
17171 });
17172 } else {
17173 for bi in 0..b {
17174 owned_state.fill(0.0);
17175 #[cfg(not(target_arch = "wasm32"))]
17176 {
17177 use rayon::prelude::*;
17178 owned_state
17179 .par_chunks_mut(n * n)
17180 .enumerate()
17181 .for_each(|(hi, s_mat)| {
17182 let mut sk_buf = [0f32; crate::gdn::GDN_MAX_STATE];
17183 run_head(bi, hi, s_mat, &mut sk_buf[..n]);
17184 });
17185 }
17186 #[cfg(target_arch = "wasm32")]
17187 {
17188 owned_state
17189 .chunks_mut(n * n)
17190 .enumerate()
17191 .for_each(|(hi, s_mat)| {
17192 let mut sk_buf = [0f32; crate::gdn::GDN_MAX_STATE];
17193 run_head(bi, hi, s_mat, &mut sk_buf[..n]);
17194 });
17195 }
17196 }
17197 }
17198 }
17199}
17200
17201pub unsafe fn execute_rms_norm_backward_input_f32(
17203 x: usize,
17204 gamma: usize,
17205 beta: usize,
17206 dy: usize,
17207 dx: usize,
17208 rows: u32,
17209 h: u32,
17210 eps: f32,
17211 base: *mut u8,
17212) {
17213 let (rows, h) = (rows as usize, h as usize);
17214 let mut dg = vec![0f32; h];
17215 let mut db = vec![0f32; h];
17216 let xs = sl(x, base, rows * h);
17217 let dys = sl(dy, base, rows * h);
17218 let g = sl(gamma, base, h);
17219 let b = sl(beta, base, h);
17220 let out = sl_mut(dx, base, rows * h);
17221 for r in 0..rows {
17222 crate::training_bwd::rms_norm_backward_row(
17223 &xs[r * h..(r + 1) * h],
17224 g,
17225 b,
17226 &dys[r * h..(r + 1) * h],
17227 &mut out[r * h..(r + 1) * h],
17228 &mut dg,
17229 &mut db,
17230 eps,
17231 );
17232 }
17233}
17234
17235pub unsafe fn execute_rms_norm_backward_gamma_f32(
17236 x: usize,
17237 gamma: usize,
17238 beta: usize,
17239 dy: usize,
17240 dgamma: usize,
17241 rows: u32,
17242 h: u32,
17243 eps: f32,
17244 base: *mut u8,
17245) {
17246 let (rows, h) = (rows as usize, h as usize);
17247 let out = sl_mut(dgamma, base, h);
17248 out.fill(0.0);
17249 let mut dx = vec![0f32; h];
17250 let mut db = vec![0f32; h];
17251 let xs = sl(x, base, rows * h);
17252 let dys = sl(dy, base, rows * h);
17253 let g = sl(gamma, base, h);
17254 let b = sl(beta, base, h);
17255 for r in 0..rows {
17256 crate::training_bwd::rms_norm_backward_row(
17257 &xs[r * h..(r + 1) * h],
17258 g,
17259 b,
17260 &dys[r * h..(r + 1) * h],
17261 &mut dx,
17262 out,
17263 &mut db,
17264 eps,
17265 );
17266 }
17267}
17268
17269pub unsafe fn execute_rms_norm_backward_beta_f32(
17270 x: usize,
17271 gamma: usize,
17272 beta: usize,
17273 dy: usize,
17274 dbeta: usize,
17275 rows: u32,
17276 h: u32,
17277 eps: f32,
17278 base: *mut u8,
17279) {
17280 let (rows, h) = (rows as usize, h as usize);
17281 let out = sl_mut(dbeta, base, h);
17282 out.fill(0.0);
17283 let mut dx = vec![0f32; h];
17284 let mut dg = vec![0f32; h];
17285 let xs = sl(x, base, rows * h);
17286 let dys = sl(dy, base, rows * h);
17287 let g = sl(gamma, base, h);
17288 let b = sl(beta, base, h);
17289 for r in 0..rows {
17290 crate::training_bwd::rms_norm_backward_row(
17291 &xs[r * h..(r + 1) * h],
17292 g,
17293 b,
17294 &dys[r * h..(r + 1) * h],
17295 &mut dx,
17296 &mut dg,
17297 out,
17298 eps,
17299 );
17300 }
17301}
17302
17303#[allow(clippy::too_many_arguments)]
17304pub unsafe fn execute_conv2d_forward_f32(
17305 src: usize,
17306 weight: usize,
17307 dst: usize,
17308 n: u32,
17309 c_in: u32,
17310 h: u32,
17311 w: u32,
17312 c_out: u32,
17313 h_out: u32,
17314 w_out: u32,
17315 kh: u32,
17316 kw: u32,
17317 sh: u32,
17318 sw: u32,
17319 ph: u32,
17320 pw: u32,
17321 dh: u32,
17322 dw: u32,
17323 groups: u32,
17324 base: *mut u8,
17325) {
17326 let n = n as usize;
17327 let c_in = c_in as usize;
17328 let h = h as usize;
17329 let w = w as usize;
17330 let c_out = c_out as usize;
17331 let h_out = h_out as usize;
17332 let w_out = w_out as usize;
17333 let kh = kh as usize;
17334 let kw = kw as usize;
17335 let sh = sh as usize;
17336 let sw = sw as usize;
17337 let ph = ph as usize;
17338 let pw = pw as usize;
17339 let dh = dh as usize;
17340 let dw = dw as usize;
17341 let groups = groups as usize;
17342 let c_in_per_g = c_in / groups;
17343 let inp = sl(src, base, n * c_in * h * w);
17344 let wt = sl(weight, base, c_out * c_in_per_g * kh * kw);
17345 let out = sl_mut(dst, base, n * c_out * h_out * w_out);
17346 crate::conv_fwd::conv2d_forward_nchw_f32(
17347 inp, wt, out, n, c_in, h, w, c_out, h_out, w_out, kh, kw, sh, sw, ph, pw, dh, dw, groups,
17348 );
17349}
17350
17351pub unsafe fn execute_maxpool2d_backward_f32(
17352 x: usize,
17353 dy: usize,
17354 dx: usize,
17355 n: u32,
17356 c: u32,
17357 h: u32,
17358 w: u32,
17359 h_out: u32,
17360 w_out: u32,
17361 kh: u32,
17362 kw: u32,
17363 sh: u32,
17364 sw: u32,
17365 ph: u32,
17366 pw: u32,
17367 base: *mut u8,
17368) {
17369 let (n, c, h, w) = (n as usize, c as usize, h as usize, w as usize);
17370 let (h_out, w_out) = (h_out as usize, w_out as usize);
17371 let (kh, kw) = (kh as usize, kw as usize);
17372 let (sh, sw) = (sh as usize, sw as usize);
17373 let (ph, pw) = (ph as usize, pw as usize);
17374 let xs = sl(x, base, n * c * h * w);
17375 let dys = sl(dy, base, n * c * h_out * w_out);
17376 let dxs = sl_mut(dx, base, n * c * h * w);
17377 if fast_conv_enabled() && crate::pool::should_parallelize(n * c * h * w) {
17380 let (in_plane, out_plane) = (h * w, h_out * w_out);
17381 let x_addr = xs.as_ptr() as usize;
17382 let dy_addr = dys.as_ptr() as usize;
17383 let dx_addr = dxs.as_mut_ptr() as usize;
17384 crate::pool::par_for(n * c, crate::pool::outer_chunk(n * c), &|off, cnt| {
17385 for nc in off..off + cnt {
17386 let xp =
17387 std::slice::from_raw_parts((x_addr as *const f32).add(nc * in_plane), in_plane);
17388 let dyp = std::slice::from_raw_parts(
17389 (dy_addr as *const f32).add(nc * out_plane),
17390 out_plane,
17391 );
17392 let dxp = std::slice::from_raw_parts_mut(
17393 (dx_addr as *mut f32).add(nc * in_plane),
17394 in_plane,
17395 );
17396 crate::training_bwd::maxpool2d_backward_nchw(
17397 xp, dyp, dxp, 1, 1, h, w, h_out, w_out, kh, kw, sh, sw, ph, pw,
17398 );
17399 }
17400 });
17401 } else {
17402 crate::training_bwd::maxpool2d_backward_nchw(
17403 xs, dys, dxs, n, c, h, w, h_out, w_out, kh, kw, sh, sw, ph, pw,
17404 );
17405 }
17406}
17407
17408pub unsafe fn execute_rope_backward_f32(
17409 dy: usize,
17410 cos: usize,
17411 sin: usize,
17412 dx: usize,
17413 batch: u32,
17414 seq: u32,
17415 hidden: u32,
17416 head_dim: u32,
17417 n_rot: u32,
17418 cos_len: u32,
17419 base: *mut u8,
17420) {
17421 let (b, s, hs, dh, nr, cl) = (
17422 batch as usize,
17423 seq as usize,
17424 hidden as usize,
17425 head_dim as usize,
17426 n_rot as usize,
17427 cos_len as usize,
17428 );
17429 let nh = hs / dh;
17430 let tab_half = dh / 2;
17431 let dys = sl(dy, base, b * s * hs);
17432 let cos_tab = sl(cos, base, cl);
17433 let sin_tab = sl(sin, base, cl);
17434 let out = sl_mut(dx, base, b * s * hs);
17435 for bi in 0..b {
17436 for si in 0..s {
17437 let tab_off = si.saturating_mul(tab_half) % cl.max(1);
17438 let cp = &cos_tab[tab_off..tab_off + tab_half.min(cl)];
17439 let sp = &sin_tab[tab_off..tab_off + tab_half.min(cl)];
17440 for hi in 0..nh {
17441 let base_idx = bi * s * hs + si * hs + hi * dh;
17442 crate::training_bwd::rope_backward_row(
17443 &dys[base_idx..base_idx + dh],
17444 cp,
17445 sp,
17446 &mut out[base_idx..base_idx + dh],
17447 dh,
17448 nr,
17449 );
17450 }
17451 }
17452 }
17453}
17454
17455pub unsafe fn execute_cumsum_backward_f32(
17456 dy: usize,
17457 dx: usize,
17458 rows: u32,
17459 cols: u32,
17460 exclusive: bool,
17461 base: *mut u8,
17462) {
17463 let (rows, cols) = (rows as usize, cols as usize);
17464 let dys = sl(dy, base, rows * cols);
17465 let out = sl_mut(dx, base, rows * cols);
17466 for r in 0..rows {
17467 crate::training_bwd::cumsum_backward_row(
17468 &dys[r * cols..(r + 1) * cols],
17469 &mut out[r * cols..(r + 1) * cols],
17470 exclusive,
17471 );
17472 }
17473}
17474
17475pub unsafe fn execute_gather_backward_f32(
17476 dy: usize,
17477 indices: usize,
17478 dst: usize,
17479 outer: u32,
17480 axis_dim: u32,
17481 num_idx: u32,
17482 trailing: u32,
17483 base: *mut u8,
17484) {
17485 let (outer, axis_dim, num_idx, trailing) = (
17486 outer as usize,
17487 axis_dim as usize,
17488 num_idx as usize,
17489 trailing as usize,
17490 );
17491 let out = sl_mut(dst, base, outer * axis_dim * trailing);
17492 out.fill(0.0);
17493 crate::training_bwd::gather_axis_backward(
17494 sl(dy, base, outer * num_idx * trailing),
17495 sl(indices, base, num_idx),
17496 out,
17497 outer,
17498 axis_dim,
17499 num_idx,
17500 trailing,
17501 );
17502}
17503
17504pub unsafe fn execute_dequant_matmul_gguf_f32(
17506 x: usize,
17507 w_q: usize,
17508 dst: usize,
17509 m: usize,
17510 k: usize,
17511 n: usize,
17512 scheme: rlx_ir::quant::QuantScheme,
17513 base: *mut u8,
17514) {
17515 unsafe {
17516 let block_bytes = scheme.gguf_block_bytes() as usize;
17517 let block_elems = scheme.gguf_block_size() as usize;
17518 let total_bytes = (k * n) / block_elems * block_bytes;
17519 let xs = sl(x, base, m * k);
17520 let w_bytes = std::slice::from_raw_parts(base.add(w_q) as *const u8, total_bytes);
17521 let out = sl_mut(dst, base, m * n);
17522 crate::gguf_matmul::gguf_matmul_bt(xs, w_bytes, out, m, k, n, scheme);
17523 }
17524}
17525
17526pub unsafe fn execute_dequant_grouped_matmul_gguf_f32(
17528 input: usize,
17529 w_q: usize,
17530 expert_idx: usize,
17531 dst: usize,
17532 m: usize,
17533 k: usize,
17534 n: usize,
17535 num_experts: usize,
17536 scheme: rlx_ir::quant::QuantScheme,
17537 base: *mut u8,
17538) {
17539 unsafe {
17540 let block_bytes = scheme.gguf_block_bytes() as usize;
17541 let block_elems = scheme.gguf_block_size() as usize;
17542 let slab_bytes = (k * n) / block_elems * block_bytes;
17543 let xs = sl(input, base, m * k);
17544 let w_bytes =
17545 std::slice::from_raw_parts(base.add(w_q) as *const u8, num_experts * slab_bytes);
17546 let ids = sl(expert_idx, base, m);
17547 let out = sl_mut(dst, base, m * n);
17548 crate::gguf_matmul::gguf_grouped_matmul_bt(
17549 xs,
17550 w_bytes,
17551 ids,
17552 out,
17553 m,
17554 k,
17555 n,
17556 num_experts,
17557 scheme,
17558 );
17559 }
17560}
17561
17562pub unsafe fn execute_dequant_matmul_int8_f32(
17564 x: usize,
17565 w_q: usize,
17566 scale: usize,
17567 zp: usize,
17568 dst: usize,
17569 m: usize,
17570 k: usize,
17571 n: usize,
17572 block_size: u32,
17573 is_asymmetric: bool,
17574 base: *mut u8,
17575) {
17576 let bs = block_size as usize;
17577 let n_blocks = k.div_ceil(bs);
17578 unsafe {
17579 let xs = sl(x, base, m * k);
17580 let w_bytes = std::slice::from_raw_parts(base.add(w_q) as *const i8, k * n);
17581 let scales = sl(scale, base, n_blocks * n);
17582 let zps = if is_asymmetric {
17583 sl(zp, base, n_blocks * n)
17584 } else {
17585 &[][..]
17586 };
17587 let out = sl_mut(dst, base, m * n);
17588 dequant_matmul_int8(xs, w_bytes, scales, zps, out, m, k, n, bs, is_asymmetric);
17589 }
17590}
17591
17592pub unsafe fn execute_dequant_matmul_int4_f32(
17594 x: usize,
17595 w_q: usize,
17596 scale: usize,
17597 zp: usize,
17598 dst: usize,
17599 m: usize,
17600 k: usize,
17601 n: usize,
17602 block_size: u32,
17603 is_asymmetric: bool,
17604 base: *mut u8,
17605) {
17606 let bs = block_size as usize;
17607 let n_blocks = k.div_ceil(bs);
17608 unsafe {
17609 let xs = sl(x, base, m * k);
17610 let w_bytes = std::slice::from_raw_parts(base.add(w_q) as *const u8, (k * n).div_ceil(2));
17611 let scales = sl(scale, base, n_blocks * n);
17612 let zps = if is_asymmetric {
17613 sl(zp, base, n_blocks * n)
17614 } else {
17615 &[][..]
17616 };
17617 let out = sl_mut(dst, base, m * n);
17618 dequant_matmul_int4(xs, w_bytes, scales, zps, out, m, k, n, bs, is_asymmetric);
17619 }
17620}
17621
17622pub unsafe fn execute_dequant_matmul_fp8_f32(
17624 x: usize,
17625 w_q: usize,
17626 scale: usize,
17627 dst: usize,
17628 m: usize,
17629 k: usize,
17630 n: usize,
17631 e5m2: bool,
17632 base: *mut u8,
17633) {
17634 unsafe {
17635 let xs = sl(x, base, m * k);
17636 let w_bytes = std::slice::from_raw_parts(base.add(w_q) as *const u8, k * n);
17637 let scales = sl(scale, base, n);
17638 let out = sl_mut(dst, base, m * n);
17639 dequant_matmul_fp8(xs, w_bytes, scales, out, m, k, n, e5m2);
17640 }
17641}
17642
17643pub unsafe fn execute_dequant_matmul_nvfp4_f32(
17645 x: usize,
17646 w_q: usize,
17647 scale: usize,
17648 global_scale: usize,
17649 dst: usize,
17650 m: usize,
17651 k: usize,
17652 n: usize,
17653 base: *mut u8,
17654) {
17655 let n_scale = k.div_ceil(rlx_ir::NVFP4_GROUP_SIZE) * n;
17656 unsafe {
17657 let xs = sl(x, base, m * k);
17658 let w_bytes = std::slice::from_raw_parts(base.add(w_q) as *const u8, (k * n).div_ceil(2));
17659 let scale_bytes = std::slice::from_raw_parts(base.add(scale) as *const u8, n_scale);
17660 let gs = sl(global_scale, base, 1)[0];
17661 let out = sl_mut(dst, base, m * n);
17662 dequant_matmul_nvfp4(xs, w_bytes, scale_bytes, gs, out, m, k, n);
17663 }
17664}
17665
17666pub unsafe fn execute_scaled_quant_scale_f32(
17672 x: usize,
17673 dst: usize,
17674 rows: usize,
17675 cols: usize,
17676 fmt: rlx_ir::ScaledFormat,
17677 layout: rlx_ir::ScaleLayout,
17678 base: *mut u8,
17679) {
17680 unsafe {
17681 let xs = sl(x, base, rows * cols);
17682 let scales = lowp_compute_scales(xs, fmt, layout, rows, cols);
17683 let nblk = lowp_nblk(cols, layout);
17684 match layout {
17685 rlx_ir::ScaleLayout::PerTensor => {
17686 sl_mut(dst, base, 1)[0] = scales[0];
17687 }
17688 rlx_ir::ScaleLayout::BlockMxE8M0 { .. } => {
17689 let out = std::slice::from_raw_parts_mut(base.add(dst), rows * nblk);
17690 for (o, &s) in out.iter_mut().zip(&scales) {
17691 *o = rlx_ir::lowp_codec::f32_to_e8m0(s);
17692 }
17693 }
17694 rlx_ir::ScaleLayout::Nvfp4 { .. } => {
17695 let out = std::slice::from_raw_parts_mut(base.add(dst), rows * nblk);
17696 for (o, &s) in out.iter_mut().zip(&scales) {
17697 *o = rlx_ir::lowp_codec::encode(rlx_ir::ScaledFormat::F8E4M3, s);
17698 }
17699 }
17700 }
17701 }
17702}
17703
17704#[allow(clippy::too_many_arguments)]
17706pub unsafe fn execute_scaled_quantize_f32(
17707 x: usize,
17708 scale: usize,
17709 dst: usize,
17710 rows: usize,
17711 cols: usize,
17712 fmt: rlx_ir::ScaledFormat,
17713 layout: rlx_ir::ScaleLayout,
17714 base: *mut u8,
17715) {
17716 unsafe {
17717 let xs = sl(x, base, rows * cols);
17718 let nblk = lowp_nblk(cols, layout);
17719 let n_scale = if matches!(layout, rlx_ir::ScaleLayout::PerTensor) {
17720 1
17721 } else {
17722 rows * nblk
17723 };
17724 let scales = lowp_read_scales(layout, base, scale, n_scale);
17725 let out = std::slice::from_raw_parts_mut(base.add(dst), rows * cols);
17726 lowp_quantize(xs, &scales, fmt, layout, rows, cols, out);
17727 }
17728}
17729
17730#[allow(clippy::too_many_arguments)]
17733pub unsafe fn execute_scaled_dequantize_f32(
17734 codes: usize,
17735 scale: usize,
17736 dst: usize,
17737 rows: usize,
17738 cols: usize,
17739 fmt: rlx_ir::ScaledFormat,
17740 layout: rlx_ir::ScaleLayout,
17741 base: *mut u8,
17742) {
17743 unsafe {
17744 let nblk = lowp_nblk(cols, layout);
17745 let n_scale = if matches!(layout, rlx_ir::ScaleLayout::PerTensor) {
17746 1
17747 } else {
17748 rows * nblk
17749 };
17750 let cs = std::slice::from_raw_parts(base.add(codes), rows * cols);
17751 let scales = lowp_read_scales(layout, base, scale, n_scale);
17752 let out = std::slice::from_raw_parts_mut(base.add(dst) as *mut f32, rows * cols);
17753 lowp_dequantize(cs, &scales, fmt, layout, rows, cols, out);
17754 }
17755}
17756
17757#[allow(clippy::too_many_arguments)]
17759pub unsafe fn execute_scaled_matmul_f32(
17760 lhs: usize,
17761 rhs: usize,
17762 lhs_scale: usize,
17763 rhs_scale: usize,
17764 bias: usize,
17765 dst: usize,
17766 m: usize,
17767 k: usize,
17768 n: usize,
17769 has_bias: bool,
17770 lhs_fmt: rlx_ir::ScaledFormat,
17771 rhs_fmt: rlx_ir::ScaledFormat,
17772 layout: rlx_ir::ScaleLayout,
17773 base: *mut u8,
17774) {
17775 unsafe {
17776 let lhs_b = std::slice::from_raw_parts(base.add(lhs), m * k);
17777 let rhs_b = std::slice::from_raw_parts(base.add(rhs), n * k);
17778 let nblk = lowp_nblk(k, layout);
17779 let per_tensor = matches!(layout, rlx_ir::ScaleLayout::PerTensor);
17780 let n_l = if per_tensor { 1 } else { m * nblk };
17781 let n_r = if per_tensor { 1 } else { n * nblk };
17782 let ls = lowp_read_scales(layout, base, lhs_scale, n_l);
17783 let rs = lowp_read_scales(layout, base, rhs_scale, n_r);
17784 let bias_s = if has_bias {
17785 Some(sl(bias, base, n))
17786 } else {
17787 None
17788 };
17789 let out = sl_mut(dst, base, m * n);
17790 lowp_scaled_matmul(
17791 lhs_b, rhs_b, &ls, &rs, bias_s, out, m, n, k, layout, lhs_fmt, rhs_fmt,
17792 );
17793 }
17794}
17795
17796pub unsafe fn execute_gated_delta_net_f16(
17798 q: usize,
17799 k: usize,
17800 v: usize,
17801 g: usize,
17802 beta: usize,
17803 state: usize,
17804 dst: usize,
17805 batch: usize,
17806 seq: usize,
17807 heads: usize,
17808 state_size: usize,
17809 base: *mut u8,
17810) {
17811 use half::f16;
17812 unsafe {
17813 let read_f16 = |off: usize, len: usize| -> Vec<f32> {
17814 let raw = std::slice::from_raw_parts(base.add(off) as *const u8, len * 2);
17815 raw.chunks_exact(2)
17816 .map(|c| f16::from_le_bytes([c[0], c[1]]).to_f32())
17817 .collect()
17818 };
17819 let write_f16 = |off: usize, data: &[f32]| {
17820 let out = std::slice::from_raw_parts_mut(base.add(off), data.len() * 2);
17821 for (i, &v) in data.iter().enumerate() {
17822 let le = f16::from_f32(v).to_le_bytes();
17823 out[i * 2] = le[0];
17824 out[i * 2 + 1] = le[1];
17825 }
17826 };
17827
17828 let (b, s, h, n) = (batch, seq, heads, state_size);
17829 let q_f = read_f16(q, b * s * h * n);
17830 let k_f = read_f16(k, b * s * h * n);
17831 let v_f = read_f16(v, b * s * h * n);
17832 let g_f = read_f16(g, b * s * h);
17833 let b_f = read_f16(beta, b * s * h);
17834 let mut state_f = if state != 0 {
17835 read_f16(state, b * h * n * n)
17836 } else {
17837 vec![0f32; b * h * n * n]
17838 };
17839 let mut out_f = vec![0f32; b * s * h * n];
17840 let scale = 1.0f32 / (n as f32).sqrt();
17841 let mut sk_buf = vec![0f32; n];
17842 let mut owned_state = vec![0f32; h * n * n];
17843
17844 for bi in 0..b {
17845 let state_slice: &mut [f32] = if state != 0 {
17846 let start = bi * h * n * n;
17847 &mut state_f[start..start + h * n * n]
17848 } else {
17849 owned_state.fill(0.0);
17850 &mut owned_state
17851 };
17852
17853 for ti in 0..s {
17854 let qkv_step_base = bi * s * h * n + ti * h * n;
17855 let gb_step_base = bi * s * h + ti * h;
17856
17857 for hi in 0..h {
17858 let q_row = &q_f[qkv_step_base + hi * n..qkv_step_base + (hi + 1) * n];
17859 let k_row = &k_f[qkv_step_base + hi * n..qkv_step_base + (hi + 1) * n];
17860 let v_row = &v_f[qkv_step_base + hi * n..qkv_step_base + (hi + 1) * n];
17861 let g_t = g_f[gb_step_base + hi];
17862 let beta_t = b_f[gb_step_base + hi];
17863
17864 let s_base = hi * n * n;
17865 let s_mat = &mut state_slice[s_base..s_base + n * n];
17866
17867 let g_exp = g_t.exp();
17868 for st in s_mat.iter_mut() {
17869 *st *= g_exp;
17870 }
17871
17872 for j in 0..n {
17873 let mut acc = 0f32;
17874 for i in 0..n {
17875 acc += s_mat[i * n + j] * k_row[i];
17876 }
17877 sk_buf[j] = acc;
17878 }
17879
17880 for j in 0..n {
17881 sk_buf[j] = (v_row[j] - sk_buf[j]) * beta_t;
17882 }
17883
17884 for i in 0..n {
17885 let ki = k_row[i];
17886 for j in 0..n {
17887 s_mat[i * n + j] += ki * sk_buf[j];
17888 }
17889 }
17890
17891 let out_row = &mut out_f[qkv_step_base + hi * n..qkv_step_base + (hi + 1) * n];
17892 for j in 0..n {
17893 let mut acc = 0f32;
17894 for i in 0..n {
17895 acc += s_mat[i * n + j] * q_row[i];
17896 }
17897 out_row[j] = acc * scale;
17898 }
17899 }
17900 }
17901 }
17902
17903 write_f16(dst, &out_f);
17904 if state != 0 {
17905 write_f16(state, &state_f);
17906 }
17907 }
17908}
17909
17910pub unsafe fn execute_group_norm_nchw_f32(
17912 src: usize,
17913 g: usize,
17914 b: usize,
17915 dst: usize,
17916 n: usize,
17917 c: usize,
17918 h: usize,
17919 w: usize,
17920 num_groups: usize,
17921 eps: f32,
17922 base: *mut u8,
17923) {
17924 let plane = c * h * w;
17925 for ni in 0..n {
17926 let input = unsafe { sl(src + ni * plane * std::mem::size_of::<f32>(), base, plane) };
17927 let gamma = unsafe { sl(g, base, c) };
17928 let beta = unsafe { sl(b, base, c) };
17929 let output = unsafe { sl_mut(dst + ni * plane * std::mem::size_of::<f32>(), base, plane) };
17930 crate::kernels::group_norm_nchw(input, gamma, beta, output, 1, c, h, w, num_groups, eps);
17931 }
17932}
17933
17934pub unsafe fn execute_layer_norm2d_nchw_f32(
17936 src: usize,
17937 g: usize,
17938 b: usize,
17939 dst: usize,
17940 n: usize,
17941 c: usize,
17942 h: usize,
17943 w: usize,
17944 eps: f32,
17945 base: *mut u8,
17946) {
17947 let plane = c * h * w;
17948 unsafe {
17949 let input = sl(src, base, n * plane);
17950 let gamma = sl(g, base, c);
17951 let beta = sl(b, base, c);
17952 let output = sl_mut(dst, base, n * plane);
17953 crate::kernels::layer_norm2d_nchw(input, gamma, beta, output, n, c, h, w, eps);
17954 }
17955}
17956
17957pub unsafe fn execute_conv_transpose2d_nchw_f32(
17959 src: usize,
17960 weight: usize,
17961 dst: usize,
17962 n: usize,
17963 c_in: usize,
17964 h: usize,
17965 w_in: usize,
17966 c_out: usize,
17967 h_out: usize,
17968 w_out: usize,
17969 kh: usize,
17970 kw: usize,
17971 sh: usize,
17972 sw: usize,
17973 ph: usize,
17974 pw: usize,
17975 dh: usize,
17976 dw: usize,
17977 groups: usize,
17978 base: *mut u8,
17979) {
17980 let in_elems = n * c_in * h * w_in;
17981 let w_elems = c_in * (c_out / groups) * kh * kw;
17982 let out_elems = n * c_out * h_out * w_out;
17983 unsafe {
17984 let input = sl(src, base, in_elems);
17985 let wt = sl(weight, base, w_elems);
17986 let output = sl_mut(dst, base, out_elems);
17987 crate::kernels::conv_transpose2d_nchw(
17988 input, wt, output, n, c_in, h, w_in, c_out, h_out, w_out, kh, kw, sh, sw, ph, pw, dh,
17989 dw, groups,
17990 );
17991 }
17992}
17993
17994pub unsafe fn execute_resize_nearest_2x_f32(
17996 src: usize,
17997 dst: usize,
17998 n: usize,
17999 c: usize,
18000 h: usize,
18001 w: usize,
18002 base: *mut u8,
18003) {
18004 let in_plane = c * h * w;
18005 let out_plane = c * h * 2 * w * 2;
18006 for ni in 0..n {
18007 let input = unsafe {
18008 sl(
18009 src + ni * in_plane * std::mem::size_of::<f32>(),
18010 base,
18011 in_plane,
18012 )
18013 };
18014 let output = unsafe {
18015 sl_mut(
18016 dst + ni * out_plane * std::mem::size_of::<f32>(),
18017 base,
18018 out_plane,
18019 )
18020 };
18021 crate::kernels::resize_nearest_2x_nchw(input, output, c, h, w);
18022 }
18023}
18024
18025pub unsafe fn execute_axial_rope2d_f32(
18027 src: usize,
18028 dst: usize,
18029 batch: usize,
18030 seq: usize,
18031 hidden: usize,
18032 end_x: usize,
18033 end_y: usize,
18034 head_dim: usize,
18035 num_heads: usize,
18036 theta: f32,
18037 repeat_factor: usize,
18038 base: *mut u8,
18039) {
18040 let plane = seq * hidden;
18041 let plane_bytes = plane * std::mem::size_of::<f32>();
18042 for bi in 0..batch {
18043 let in_off = src + bi * plane_bytes;
18044 let input = unsafe { sl(in_off, base, plane) };
18045 let rotated = rlx_ir::ops::axial_rope2d::apply_axial_rope2d(
18046 input,
18047 num_heads,
18048 seq,
18049 head_dim,
18050 end_x,
18051 end_y,
18052 theta,
18053 repeat_factor,
18054 );
18055 let out_off = dst + bi * plane_bytes;
18056 let output = unsafe { sl_mut(out_off, base, plane) };
18057 output.copy_from_slice(&rotated);
18058 }
18059}
18060
18061pub unsafe fn execute_fft_butterfly_stage_f32(
18063 state_src: usize,
18064 state_dst: usize,
18065 gate_src: usize,
18066 rev_src: usize,
18067 tw_re_src: usize,
18068 tw_im_src: usize,
18069 batch: usize,
18070 n_fft: usize,
18071 stage: usize,
18072 base: *mut u8,
18073) {
18074 let half = n_fft / 2;
18075 let stride = 1usize << stage;
18076 let gate = unsafe { sl(gate_src, base, half) };
18077 let rev = unsafe { sl(rev_src, base, half) };
18078 let tw_re = unsafe { sl(tw_re_src, base, half) };
18079 let tw_im = unsafe { sl(tw_im_src, base, half) };
18080 let row_elems = n_fft * 2;
18081 for b in 0..batch {
18082 let in_off = state_src + b * row_elems * std::mem::size_of::<f32>();
18083 let out_off = state_dst + b * row_elems * std::mem::size_of::<f32>();
18084 let inp = unsafe { sl(in_off, base, row_elems) };
18085 let out = unsafe { sl_mut(out_off, base, row_elems) };
18086 out.copy_from_slice(inp);
18087 for bf in 0..half {
18088 if gate[bf] == 0.0 {
18089 continue;
18090 }
18091 let group = bf / stride;
18092 let k = bf % stride;
18093 let i0 = group * 2 * stride + k;
18094 let i1 = i0 + stride;
18095 let w_re = tw_re[bf];
18096 let w_im = tw_im[bf];
18097 let in_a_re = inp[i0 * 2];
18098 let in_a_im = inp[i0 * 2 + 1];
18099 let in_b_re = inp[i1 * 2];
18100 let in_b_im = inp[i1 * 2 + 1];
18101 let (b_re, b_im) = (
18102 in_b_re * w_re - in_b_im * w_im,
18103 in_b_re * w_im + in_b_im * w_re,
18104 );
18105 let (top_re, top_im) = (in_a_re + b_re, in_a_im + b_im);
18106 let (bot_re, bot_im) = (in_a_re - b_re, in_a_im - b_im);
18107 let (oa_re, oa_im, ob_re, ob_im) = if rev[bf] >= 0.5 {
18108 (bot_re, bot_im, top_re, top_im)
18109 } else {
18110 (top_re, top_im, bot_re, bot_im)
18111 };
18112 out[i0 * 2] = oa_re;
18113 out[i0 * 2 + 1] = oa_im;
18114 out[i1 * 2] = ob_re;
18115 out[i1 * 2 + 1] = ob_im;
18116 }
18117 }
18118}
18119
18120pub unsafe fn execute_fft1d_f32(
18122 src: usize,
18123 dst: usize,
18124 outer: usize,
18125 n_complex: usize,
18126 inverse: bool,
18127 norm_tag: u32,
18128 base: *mut u8,
18129) {
18130 let row_elems = 2 * n_complex;
18131 let mut re = vec![0f32; n_complex];
18132 let mut im = vec![0f32; n_complex];
18133 let norm = rlx_ir::fft::FftNorm::from_tag(norm_tag);
18134 let scale = norm.output_scale(n_complex, inverse) as f32;
18135 let mut scratch = if n_complex.is_power_of_two() || n_complex <= 16 {
18136 BluesteinScratchF32::empty()
18137 } else {
18138 BluesteinScratchF32::build(n_complex, inverse)
18139 };
18140 for o in 0..outer {
18141 let row_offset = src + o * row_elems * std::mem::size_of::<f32>();
18142 let s = unsafe { sl(row_offset, base, row_elems) };
18143 re.copy_from_slice(&s[..n_complex]);
18144 im.copy_from_slice(&s[n_complex..]);
18145 if n_complex.is_power_of_two() {
18146 fft_radix2_inplace_f32(&mut re, &mut im, inverse);
18147 } else if n_complex <= 16 {
18148 fft_naive_inplace_f32(&mut re, &mut im, inverse);
18149 } else {
18150 fft_bluestein_inplace_f32(&mut re, &mut im, inverse, &mut scratch);
18151 }
18152 if scale != 1.0 {
18153 re.iter_mut().for_each(|v| *v *= scale);
18154 im.iter_mut().for_each(|v| *v *= scale);
18155 }
18156 let dst_offset = dst + o * row_elems * std::mem::size_of::<f32>();
18157 let d = unsafe { sl_mut(dst_offset, base, row_elems) };
18158 d[..n_complex].copy_from_slice(&re);
18159 d[n_complex..].copy_from_slice(&im);
18160 }
18161}
18162
18163pub unsafe fn execute_fft1d_c64(
18165 src: usize,
18166 dst: usize,
18167 outer: usize,
18168 n_complex: usize,
18169 inverse: bool,
18170 norm_tag: u32,
18171 base: *mut u8,
18172) {
18173 let row_bytes = n_complex * 8;
18174 let mut re = vec![0f32; n_complex];
18175 let mut im = vec![0f32; n_complex];
18176 let norm = rlx_ir::fft::FftNorm::from_tag(norm_tag);
18177 let scale = norm.output_scale(n_complex, inverse) as f32;
18178 let mut scratch = if n_complex.is_power_of_two() || n_complex <= 16 {
18179 BluesteinScratchF32::empty()
18180 } else {
18181 BluesteinScratchF32::build(n_complex, inverse)
18182 };
18183 for o in 0..outer {
18184 let row_offset = src + o * row_bytes;
18185 for i in 0..n_complex {
18186 let elem_off = row_offset + i * 8;
18187 re[i] = f32::from_le_bytes([
18188 *base.add(elem_off),
18189 *base.add(elem_off + 1),
18190 *base.add(elem_off + 2),
18191 *base.add(elem_off + 3),
18192 ]);
18193 im[i] = f32::from_le_bytes([
18194 *base.add(elem_off + 4),
18195 *base.add(elem_off + 5),
18196 *base.add(elem_off + 6),
18197 *base.add(elem_off + 7),
18198 ]);
18199 }
18200 if n_complex.is_power_of_two() {
18201 fft_radix2_inplace_f32(&mut re, &mut im, inverse);
18202 } else if n_complex <= 16 {
18203 fft_naive_inplace_f32(&mut re, &mut im, inverse);
18204 } else {
18205 fft_bluestein_inplace_f32(&mut re, &mut im, inverse, &mut scratch);
18206 }
18207 if scale != 1.0 {
18208 re.iter_mut().for_each(|v| *v *= scale);
18209 im.iter_mut().for_each(|v| *v *= scale);
18210 }
18211 let dst_row = dst + o * row_bytes;
18212 for i in 0..n_complex {
18213 let elem_off = dst_row + i * 8;
18214 let re_b = re[i].to_le_bytes();
18215 let im_b = im[i].to_le_bytes();
18216 for j in 0..4 {
18217 *base.add(elem_off + j) = re_b[j];
18218 *base.add(elem_off + 4 + j) = im_b[j];
18219 }
18220 }
18221 }
18222}
18223
18224pub unsafe fn execute_log_mel(
18226 spec: usize,
18227 filters: usize,
18228 dst: usize,
18229 outer: usize,
18230 n_fft: usize,
18231 n_bins: usize,
18232 n_mels: usize,
18233 base: *mut u8,
18234) {
18235 execute_log_mel_f32(spec, filters, dst, outer, n_fft, n_bins, n_mels, base);
18236}
18237
18238pub unsafe fn execute_log_mel_f32(
18239 spec: usize,
18240 filters: usize,
18241 dst: usize,
18242 outer: usize,
18243 n_fft: usize,
18244 n_bins: usize,
18245 n_mels: usize,
18246 base: *mut u8,
18247) {
18248 let spec_ptr = base.add(spec) as *const f32;
18249 let filt_ptr = base.add(filters) as *const f32;
18250 let dst_ptr = base.add(dst) as *mut f32;
18251 let spec = std::slice::from_raw_parts(spec_ptr, outer * n_fft * 2);
18252 let filters = std::slice::from_raw_parts(filt_ptr, n_mels * n_bins);
18253 let out = std::slice::from_raw_parts_mut(dst_ptr, outer * n_mels);
18254 rlx_ir::audio::log_mel_block_f32(spec, filters, outer, n_fft, n_bins, n_mels, out);
18255}
18256
18257pub unsafe fn execute_welch_peaks_f32(
18258 spec: usize,
18259 dst: usize,
18260 welch_batch: usize,
18261 n_fft: usize,
18262 n_segments: usize,
18263 k: usize,
18264 base: *mut u8,
18265) {
18266 let spec_ptr = base.add(spec) as *const f32;
18267 let dst_ptr = base.add(dst) as *mut f32;
18268 let outer = welch_batch * n_segments;
18269 let spec = std::slice::from_raw_parts(spec_ptr, outer * n_fft * 2);
18270 let out = std::slice::from_raw_parts_mut(dst_ptr, welch_batch * k * 2);
18271 rlx_ir::audio::welch_peaks_block_f32(spec, welch_batch, n_fft, n_segments, k, out);
18272}
18273
18274pub unsafe fn execute_log_mel_backward_f32(
18275 spec: usize,
18276 filters: usize,
18277 dy: usize,
18278 dst: usize,
18279 outer: usize,
18280 n_fft: usize,
18281 n_bins: usize,
18282 n_mels: usize,
18283 base: *mut u8,
18284) {
18285 let spec_ptr = base.add(spec) as *const f32;
18286 let filt_ptr = base.add(filters) as *const f32;
18287 let dy_ptr = base.add(dy) as *const f32;
18288 let dst_ptr = base.add(dst) as *mut f32;
18289 let spec = std::slice::from_raw_parts(spec_ptr, outer * n_fft * 2);
18290 let filters = std::slice::from_raw_parts(filt_ptr, n_mels * n_bins);
18291 let dy = std::slice::from_raw_parts(dy_ptr, outer * n_mels);
18292 let d_spec = std::slice::from_raw_parts_mut(dst_ptr, outer * n_fft * 2);
18293 d_spec.fill(0.0);
18294 rlx_ir::audio::log_mel_block_vjp(spec, filters, dy, outer, n_fft, n_bins, n_mels, d_spec);
18295}
18296
18297pub unsafe fn execute_fft1d(
18299 src: usize,
18300 dst: usize,
18301 outer: usize,
18302 n_complex: usize,
18303 inverse: bool,
18304 norm_tag: u32,
18305 dtype: rlx_ir::DType,
18306 base: *mut u8,
18307) {
18308 match dtype {
18309 rlx_ir::DType::F32 => {
18310 execute_fft1d_f32(src, dst, outer, n_complex, inverse, norm_tag, base)
18311 }
18312 rlx_ir::DType::F64 => {
18313 execute_fft1d_f64(src, dst, outer, n_complex, inverse, norm_tag, base)
18314 }
18315 rlx_ir::DType::C64 => {
18316 execute_fft1d_c64(src, dst, outer, n_complex, inverse, norm_tag, base)
18317 }
18318 other => panic!("execute_fft1d: unsupported dtype {other:?}"),
18319 }
18320}
18321
18322fn fft_radix2_inplace_f32(re: &mut [f32], im: &mut [f32], inverse: bool) {
18327 let n = re.len();
18328 debug_assert_eq!(im.len(), n);
18329 debug_assert!(
18330 n.is_power_of_two(),
18331 "fft_radix2_f32: n={n} must be a power of two"
18332 );
18333 if n <= 1 {
18334 return;
18335 }
18336
18337 let mut j = 0usize;
18338 for i in 1..n {
18339 let mut bit = n >> 1;
18340 while j & bit != 0 {
18341 j ^= bit;
18342 bit >>= 1;
18343 }
18344 j ^= bit;
18345 if i < j {
18346 re.swap(i, j);
18347 im.swap(i, j);
18348 }
18349 }
18350
18351 let sign = if inverse { 1.0_f64 } else { -1.0_f64 };
18352 let mut len = 2usize;
18353 while len <= n {
18354 let half = len / 2;
18355 let theta = sign * 2.0 * std::f64::consts::PI / (len as f64);
18356 let w_re_step = theta.cos();
18357 let w_im_step = theta.sin();
18358 let mut i = 0usize;
18359 while i < n {
18360 let mut wre = 1.0_f64;
18361 let mut wim = 0.0_f64;
18362 for k in 0..half {
18363 let wre_f = wre as f32;
18364 let wim_f = wim as f32;
18365 let t_re = wre_f * re[i + k + half] - wim_f * im[i + k + half];
18366 let t_im = wre_f * im[i + k + half] + wim_f * re[i + k + half];
18367 let u_re = re[i + k];
18368 let u_im = im[i + k];
18369 re[i + k] = u_re + t_re;
18370 im[i + k] = u_im + t_im;
18371 re[i + k + half] = u_re - t_re;
18372 im[i + k + half] = u_im - t_im;
18373 let new_wre = wre * w_re_step - wim * w_im_step;
18374 let new_wim = wre * w_im_step + wim * w_re_step;
18375 wre = new_wre;
18376 wim = new_wim;
18377 }
18378 i += len;
18379 }
18380 len <<= 1;
18381 }
18382}
18383
18384fn fft_radix2_inplace_f64(re: &mut [f64], im: &mut [f64], inverse: bool) {
18388 let n = re.len();
18389 debug_assert_eq!(im.len(), n);
18390 debug_assert!(
18391 n.is_power_of_two(),
18392 "fft_radix2: n={n} must be a power of two"
18393 );
18394 if n <= 1 {
18395 return;
18396 }
18397
18398 let mut j = 0usize;
18400 for i in 1..n {
18401 let mut bit = n >> 1;
18402 while j & bit != 0 {
18403 j ^= bit;
18404 bit >>= 1;
18405 }
18406 j ^= bit;
18407 if i < j {
18408 re.swap(i, j);
18409 im.swap(i, j);
18410 }
18411 }
18412
18413 let sign = if inverse { 1.0 } else { -1.0 };
18415 let mut len = 2usize;
18416 while len <= n {
18417 let half = len / 2;
18418 let theta = sign * 2.0 * std::f64::consts::PI / (len as f64);
18419 let w_re_step = theta.cos();
18420 let w_im_step = theta.sin();
18421 let mut i = 0usize;
18422 while i < n {
18423 let mut wre = 1.0_f64;
18425 let mut wim = 0.0_f64;
18426 for k in 0..half {
18427 let t_re = wre * re[i + k + half] - wim * im[i + k + half];
18428 let t_im = wre * im[i + k + half] + wim * re[i + k + half];
18429 let u_re = re[i + k];
18430 let u_im = im[i + k];
18431 re[i + k] = u_re + t_re;
18432 im[i + k] = u_im + t_im;
18433 re[i + k + half] = u_re - t_re;
18434 im[i + k + half] = u_im - t_im;
18435 let new_wre = wre * w_re_step - wim * w_im_step;
18436 let new_wim = wre * w_im_step + wim * w_re_step;
18437 wre = new_wre;
18438 wim = new_wim;
18439 }
18440 i += len;
18441 }
18442 len <<= 1;
18443 }
18444}
18445
18446struct BluesteinScratchF64 {
18450 m: usize,
18452 w_re: Vec<f64>,
18456 w_im: Vec<f64>,
18457 bf_re: Vec<f64>,
18460 bf_im: Vec<f64>,
18461 ar: Vec<f64>,
18463 ai: Vec<f64>,
18464}
18465
18466impl BluesteinScratchF64 {
18467 fn empty() -> Self {
18468 Self {
18469 m: 0,
18470 w_re: Vec::new(),
18471 w_im: Vec::new(),
18472 bf_re: Vec::new(),
18473 bf_im: Vec::new(),
18474 ar: Vec::new(),
18475 ai: Vec::new(),
18476 }
18477 }
18478
18479 fn build(n: usize, inverse: bool) -> Self {
18480 let m = if n <= 1 {
18483 1
18484 } else {
18485 (2 * n - 1).next_power_of_two()
18486 };
18487
18488 let mod_2n = (2 * n) as u64;
18491 let sign = if inverse { 1.0_f64 } else { -1.0_f64 };
18492 let mut w_re = vec![0.0_f64; n];
18493 let mut w_im = vec![0.0_f64; n];
18494 for k in 0..n {
18495 let k2 = (k as u64).wrapping_mul(k as u64) % mod_2n;
18496 let theta = sign * std::f64::consts::PI * (k2 as f64) / (n as f64);
18497 w_re[k] = theta.cos();
18498 w_im[k] = theta.sin();
18499 }
18500
18501 let mut bf_re = vec![0.0_f64; m];
18504 let mut bf_im = vec![0.0_f64; m];
18505 if n > 0 {
18506 bf_re[0] = w_re[0];
18507 bf_im[0] = -w_im[0];
18508 for k in 1..n {
18509 bf_re[k] = w_re[k];
18510 bf_im[k] = -w_im[k];
18511 bf_re[m - k] = w_re[k];
18512 bf_im[m - k] = -w_im[k];
18513 }
18514 }
18515 if m > 1 {
18516 fft_radix2_inplace_f64(&mut bf_re, &mut bf_im, false);
18517 }
18518
18519 Self {
18520 m,
18521 w_re,
18522 w_im,
18523 bf_re,
18524 bf_im,
18525 ar: vec![0.0_f64; m],
18526 ai: vec![0.0_f64; m],
18527 }
18528 }
18529}
18530
18531fn fft_naive_inplace_f64(re: &mut [f64], im: &mut [f64], inverse: bool) {
18533 let n = re.len();
18534 if n <= 1 {
18535 return;
18536 }
18537 let sign = if inverse { 1.0 } else { -1.0 };
18538 let mut out_re = vec![0.0_f64; n];
18539 let mut out_im = vec![0.0_f64; n];
18540 for k in 0..n {
18541 for nn in 0..n {
18542 let theta = sign * 2.0 * std::f64::consts::PI * (nn as f64) * (k as f64) / (n as f64);
18543 let c = theta.cos();
18544 let s = theta.sin();
18545 out_re[k] += re[nn] * c - im[nn] * s;
18546 out_im[k] += re[nn] * s + im[nn] * c;
18547 }
18548 }
18549 re.copy_from_slice(&out_re);
18550 im.copy_from_slice(&out_im);
18551}
18552
18553fn fft_naive_inplace_f32(re: &mut [f32], im: &mut [f32], inverse: bool) {
18554 let n = re.len();
18555 if n <= 1 {
18556 return;
18557 }
18558 let sign = if inverse { 1.0f32 } else { -1.0f32 };
18559 let mut out_re = vec![0.0_f32; n];
18560 let mut out_im = vec![0.0_f32; n];
18561 for k in 0..n {
18562 for nn in 0..n {
18563 let theta = sign * 2.0 * std::f32::consts::PI * (nn as f32) * (k as f32) / (n as f32);
18564 let c = theta.cos();
18565 let s = theta.sin();
18566 out_re[k] += re[nn] * c - im[nn] * s;
18567 out_im[k] += re[nn] * s + im[nn] * c;
18568 }
18569 }
18570 re.copy_from_slice(&out_re);
18571 im.copy_from_slice(&out_im);
18572}
18573
18574fn fft_bluestein_inplace_f64(
18583 re: &mut [f64],
18584 im: &mut [f64],
18585 _inverse: bool,
18586 s: &mut BluesteinScratchF64,
18587) {
18588 let n = re.len();
18589 debug_assert_eq!(im.len(), n);
18590 debug_assert_eq!(s.w_re.len(), n);
18591 if n <= 1 {
18592 return;
18593 }
18594 let m = s.m;
18595
18596 for k in 0..m {
18598 s.ar[k] = 0.0;
18599 s.ai[k] = 0.0;
18600 }
18601 for k in 0..n {
18602 s.ar[k] = re[k] * s.w_re[k] - im[k] * s.w_im[k];
18603 s.ai[k] = re[k] * s.w_im[k] + im[k] * s.w_re[k];
18604 }
18605
18606 fft_radix2_inplace_f64(&mut s.ar, &mut s.ai, false);
18608
18609 for k in 0..m {
18611 let ar = s.ar[k];
18612 let ai = s.ai[k];
18613 let br = s.bf_re[k];
18614 let bi = s.bf_im[k];
18615 s.ar[k] = ar * br - ai * bi;
18616 s.ai[k] = ar * bi + ai * br;
18617 }
18618
18619 fft_radix2_inplace_f64(&mut s.ar, &mut s.ai, true);
18622 let inv_m = 1.0 / (m as f64);
18623
18624 for k in 0..n {
18626 let yr = s.ar[k] * inv_m;
18627 let yi = s.ai[k] * inv_m;
18628 re[k] = yr * s.w_re[k] - yi * s.w_im[k];
18629 im[k] = yr * s.w_im[k] + yi * s.w_re[k];
18630 }
18631}
18632
18633struct BluesteinScratchF32 {
18637 m: usize,
18638 w_re: Vec<f32>,
18639 w_im: Vec<f32>,
18640 bf_re: Vec<f32>,
18641 bf_im: Vec<f32>,
18642 ar: Vec<f32>,
18643 ai: Vec<f32>,
18644}
18645
18646impl BluesteinScratchF32 {
18647 fn empty() -> Self {
18648 Self {
18649 m: 0,
18650 w_re: Vec::new(),
18651 w_im: Vec::new(),
18652 bf_re: Vec::new(),
18653 bf_im: Vec::new(),
18654 ar: Vec::new(),
18655 ai: Vec::new(),
18656 }
18657 }
18658
18659 fn build(n: usize, inverse: bool) -> Self {
18660 let m = if n <= 1 {
18661 1
18662 } else {
18663 (2 * n - 1).next_power_of_two()
18664 };
18665
18666 let mod_2n = (2 * n) as u64;
18667 let sign = if inverse { 1.0_f64 } else { -1.0_f64 };
18668 let mut w_re = vec![0.0_f32; n];
18669 let mut w_im = vec![0.0_f32; n];
18670 for k in 0..n {
18671 let k2 = (k as u64).wrapping_mul(k as u64) % mod_2n;
18672 let theta = sign * std::f64::consts::PI * (k2 as f64) / (n as f64);
18673 w_re[k] = theta.cos() as f32;
18674 w_im[k] = theta.sin() as f32;
18675 }
18676
18677 let mut bf_re = vec![0.0_f32; m];
18678 let mut bf_im = vec![0.0_f32; m];
18679 if n > 0 {
18680 bf_re[0] = w_re[0];
18681 bf_im[0] = -w_im[0];
18682 for k in 1..n {
18683 bf_re[k] = w_re[k];
18684 bf_im[k] = -w_im[k];
18685 bf_re[m - k] = w_re[k];
18686 bf_im[m - k] = -w_im[k];
18687 }
18688 }
18689 if m > 1 {
18690 fft_radix2_inplace_f32(&mut bf_re, &mut bf_im, false);
18691 }
18692
18693 Self {
18694 m,
18695 w_re,
18696 w_im,
18697 bf_re,
18698 bf_im,
18699 ar: vec![0.0_f32; m],
18700 ai: vec![0.0_f32; m],
18701 }
18702 }
18703}
18704
18705fn fft_bluestein_inplace_f32(
18706 re: &mut [f32],
18707 im: &mut [f32],
18708 _inverse: bool,
18709 s: &mut BluesteinScratchF32,
18710) {
18711 let n = re.len();
18712 debug_assert_eq!(im.len(), n);
18713 debug_assert_eq!(s.w_re.len(), n);
18714 if n <= 1 {
18715 return;
18716 }
18717 let m = s.m;
18718
18719 for k in 0..m {
18720 s.ar[k] = 0.0;
18721 s.ai[k] = 0.0;
18722 }
18723 for k in 0..n {
18724 s.ar[k] = re[k] * s.w_re[k] - im[k] * s.w_im[k];
18725 s.ai[k] = re[k] * s.w_im[k] + im[k] * s.w_re[k];
18726 }
18727
18728 fft_radix2_inplace_f32(&mut s.ar, &mut s.ai, false);
18729
18730 for k in 0..m {
18731 let ar = s.ar[k];
18732 let ai = s.ai[k];
18733 let br = s.bf_re[k];
18734 let bi = s.bf_im[k];
18735 s.ar[k] = ar * br - ai * bi;
18736 s.ai[k] = ar * bi + ai * br;
18737 }
18738
18739 fft_radix2_inplace_f32(&mut s.ar, &mut s.ai, true);
18740 let inv_m = 1.0_f32 / (m as f32);
18741
18742 for k in 0..n {
18743 let yr = s.ar[k] * inv_m;
18744 let yi = s.ai[k] * inv_m;
18745 re[k] = yr * s.w_re[k] - yi * s.w_im[k];
18746 im[k] = yr * s.w_im[k] + yi * s.w_re[k];
18747 }
18748}
18749
18750unsafe fn dispatch_custom_op(
18756 kernel: &dyn crate::op_registry::CpuKernel,
18757 inputs: &[(usize, u32, Shape)],
18758 out_off: usize,
18759 out_len: u32,
18760 out_shape: &Shape,
18761 attrs: &[u8],
18762 base: *mut u8,
18763) {
18764 use crate::op_registry::{CpuTensorMut, CpuTensorRef};
18765 use rlx_ir::DType;
18766
18767 macro_rules! build_in_view {
18772 ($shape:expr, $off:expr, $n:expr, $variant:ident, $rust_ty:ty) => {
18773 CpuTensorRef::$variant {
18774 data: unsafe { sl_typed::<$rust_ty>($off, base, $n) },
18775 shape: $shape,
18776 }
18777 };
18778 }
18779 macro_rules! build_out_view {
18780 ($variant:ident, $rust_ty:ty) => {
18781 CpuTensorMut::$variant {
18782 data: unsafe { sl_mut_typed::<$rust_ty>(out_off, base, out_len as usize) },
18783 shape: out_shape,
18784 }
18785 };
18786 }
18787
18788 let in_views: Vec<CpuTensorRef<'_>> = inputs
18789 .iter()
18790 .map(|(off, len, shape)| {
18791 let n = *len as usize;
18792 let off = *off;
18793 match shape.dtype() {
18794 DType::F32 => build_in_view!(shape, off, n, F32, f32),
18795 DType::F64 => build_in_view!(shape, off, n, F64, f64),
18796 DType::F16 => build_in_view!(shape, off, n, F16, half::f16),
18797 DType::BF16 => build_in_view!(shape, off, n, BF16, half::bf16),
18798 DType::I8 => build_in_view!(shape, off, n, I8, i8),
18799 DType::I16 => build_in_view!(shape, off, n, I16, i16),
18800 DType::I32 => build_in_view!(shape, off, n, I32, i32),
18801 DType::I64 => build_in_view!(shape, off, n, I64, i64),
18802 DType::U8 => build_in_view!(shape, off, n, U8, u8),
18803 DType::U32 => build_in_view!(shape, off, n, U32, u32),
18804 DType::Bool => build_in_view!(shape, off, n, Bool, u8),
18805 DType::C64 => panic!(
18809 "Op::Custom kernel input has DType::C64 — built-in \
18810 complex ops handle their own kernels; user-registered \
18811 ops don't yet see complex tensors"
18812 ),
18813 }
18814 })
18815 .collect();
18816
18817 let result = match out_shape.dtype() {
18818 DType::F32 => kernel.execute(&in_views, build_out_view!(F32, f32), attrs),
18819 DType::F64 => kernel.execute(&in_views, build_out_view!(F64, f64), attrs),
18820 DType::F16 => kernel.execute(&in_views, build_out_view!(F16, half::f16), attrs),
18821 DType::BF16 => kernel.execute(&in_views, build_out_view!(BF16, half::bf16), attrs),
18822 DType::I8 => kernel.execute(&in_views, build_out_view!(I8, i8), attrs),
18823 DType::I16 => kernel.execute(&in_views, build_out_view!(I16, i16), attrs),
18824 DType::I32 => kernel.execute(&in_views, build_out_view!(I32, i32), attrs),
18825 DType::I64 => kernel.execute(&in_views, build_out_view!(I64, i64), attrs),
18826 DType::U8 => kernel.execute(&in_views, build_out_view!(U8, u8), attrs),
18827 DType::U32 => kernel.execute(&in_views, build_out_view!(U32, u32), attrs),
18828 DType::Bool => kernel.execute(&in_views, build_out_view!(Bool, u8), attrs),
18829 DType::C64 => panic!("Op::Custom output DType::C64 not supported"),
18830 };
18831 if let Err(e) = result {
18832 panic!("Op::Custom('{}') CPU kernel failed: {e}", kernel.name());
18833 }
18834}
18835
18836#[inline(always)]
18842unsafe fn sl_typed<T>(offset: usize, base: *mut u8, len: usize) -> &'static [T] {
18843 if offset == usize::MAX {
18844 return &[];
18845 }
18846 unsafe { std::slice::from_raw_parts(base.add(offset) as *const T, len) }
18847}
18848
18849#[inline(always)]
18850unsafe fn sl_mut_typed<T>(offset: usize, base: *mut u8, len: usize) -> &'static mut [T] {
18851 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut T, len) }
18852}
18853
18854#[inline]
18857fn region_activation_scalar(act: rlx_ir::op::Activation, x: f32) -> f32 {
18858 use rlx_ir::op::Activation as A;
18859 const GC: f32 = 0.797_884_6; match act {
18861 A::Relu => x.max(0.0),
18862 A::Gelu | A::GeluApprox => 0.5 * x * (1.0 + (GC * (x + 0.044715 * x * x * x)).tanh()),
18863 A::Silu => x / (1.0 + (-x).exp()),
18864 A::Sigmoid => 1.0 / (1.0 + (-x).exp()),
18865 A::Tanh => x.tanh(),
18866 A::Exp => x.exp(),
18867 A::Log => x.ln(),
18868 A::Sqrt => x.sqrt(),
18869 A::Rsqrt => 1.0 / x.sqrt(),
18870 A::Neg => -x,
18871 A::Abs => x.abs(),
18872 A::Sin => x.sin(),
18873 A::Cos => x.cos(),
18874 A::Tan => x.tan(),
18875 A::Atan => x.atan(),
18876 A::Round => x.round(),
18877 }
18878}
18879
18880#[inline]
18881fn region_binary_scalar(op: rlx_ir::op::BinaryOp, l: f32, r: f32) -> f32 {
18882 use rlx_ir::op::BinaryOp as B;
18883 match op {
18884 B::Add => l + r,
18885 B::Sub => l - r,
18886 B::Mul => l * r,
18887 B::Div => l / r,
18888 B::Max => l.max(r),
18889 B::Min => l.min(r),
18890 B::Pow => l.powf(r),
18891 }
18892}
18893
18894#[inline]
18895fn region_compare_scalar(op: rlx_ir::op::CmpOp, l: f32, r: f32) -> bool {
18896 use rlx_ir::op::CmpOp as C;
18897 match op {
18898 C::Eq => l == r,
18899 C::Ne => l != r,
18900 C::Lt => l < r,
18901 C::Le => l <= r,
18902 C::Gt => l > r,
18903 C::Ge => l >= r,
18904 }
18905}
18906
18907#[inline]
18911fn region_resolve_operand(
18912 op: &rlx_ir::op::ChainOperand,
18913 gid: usize,
18914 base: *const u8,
18915 input_offs: &[usize],
18916 scalar_mask: u32,
18917 modulus: &[u32; 16],
18918 scratch: &[f32; 32],
18919) -> f32 {
18920 use rlx_ir::op::ChainOperand as O;
18921 match op {
18922 O::Step(s) => scratch[*s as usize],
18923 O::Input(i) => {
18924 let i = *i as usize;
18925 let row = if (scalar_mask >> i) & 1 == 1 {
18926 0
18927 } else if modulus[i] != 0 {
18928 gid % modulus[i] as usize
18929 } else {
18930 gid
18931 };
18932 unsafe { *(base.add(input_offs[i]) as *const f32).add(row) }
18933 }
18934 }
18935}
18936
18937#[inline]
18940fn region_eval_elem(
18941 gid: usize,
18942 base: *const u8,
18943 input_offs: &[usize],
18944 chain: &[rlx_ir::op::ChainStep],
18945 scalar_mask: u32,
18946 modulus: &[u32; 16],
18947) -> f32 {
18948 use rlx_ir::op::ChainStep as S;
18949 let mut scratch = [0f32; 32];
18950 let r = |o: &rlx_ir::op::ChainOperand, sc: &[f32; 32]| {
18951 region_resolve_operand(o, gid, base, input_offs, scalar_mask, modulus, sc)
18952 };
18953 for (k, step) in chain.iter().enumerate() {
18954 scratch[k] = match step {
18955 S::Activation(a, x) => region_activation_scalar(*a, r(x, &scratch)),
18956 S::Cast(_, x) => r(x, &scratch), S::Binary(op, l, rr) => region_binary_scalar(*op, r(l, &scratch), r(rr, &scratch)),
18958 S::Compare(op, l, rr) => {
18959 if region_compare_scalar(*op, r(l, &scratch), r(rr, &scratch)) {
18960 1.0
18961 } else {
18962 0.0
18963 }
18964 }
18965 S::Where(c, t, f) => {
18966 if r(c, &scratch) != 0.0 {
18967 r(t, &scratch)
18968 } else {
18969 r(f, &scratch)
18970 }
18971 }
18972 };
18973 }
18974 scratch[chain.len() - 1]
18975}
18976
18977#[inline(always)]
18982fn apply_activation_inplace(d: &mut [f32], act: rlx_ir::op::Activation) {
18983 use rlx_ir::op::Activation;
18984 match act {
18985 Activation::Gelu => crate::kernels::par_gelu_inplace(d),
18986 Activation::GeluApprox => crate::kernels::par_gelu_approx_inplace(d),
18987 Activation::Silu => crate::kernels::par_silu_inplace(d),
18988 Activation::Relu => {
18989 for v in d.iter_mut() {
18990 *v = v.max(0.0);
18991 }
18992 }
18993 Activation::Sigmoid => {
18994 for v in d.iter_mut() {
18995 *v = 1.0 / (1.0 + (-*v).exp());
18996 }
18997 }
18998 Activation::Tanh => {
18999 for v in d.iter_mut() {
19000 *v = v.tanh();
19001 }
19002 }
19003 Activation::Exp => {
19004 for v in d.iter_mut() {
19005 *v = v.exp();
19006 }
19007 }
19008 Activation::Log => {
19009 for v in d.iter_mut() {
19010 *v = v.ln();
19011 }
19012 }
19013 Activation::Sqrt => {
19014 for v in d.iter_mut() {
19015 *v = v.sqrt();
19016 }
19017 }
19018 Activation::Rsqrt => {
19019 for v in d.iter_mut() {
19020 *v = 1.0 / v.sqrt();
19021 }
19022 }
19023 Activation::Neg => {
19024 for v in d.iter_mut() {
19025 *v = -*v;
19026 }
19027 }
19028 Activation::Abs => {
19029 for v in d.iter_mut() {
19030 *v = v.abs();
19031 }
19032 }
19033 Activation::Round => {
19034 for v in d.iter_mut() {
19035 *v = v.round();
19036 }
19037 }
19038 Activation::Sin => {
19039 for v in d.iter_mut() {
19040 *v = v.sin();
19041 }
19042 }
19043 Activation::Cos => {
19044 for v in d.iter_mut() {
19045 *v = v.cos();
19046 }
19047 }
19048 Activation::Tan => {
19049 for v in d.iter_mut() {
19050 *v = v.tan();
19051 }
19052 }
19053 Activation::Atan => {
19054 for v in d.iter_mut() {
19055 *v = v.atan();
19056 }
19057 }
19058 }
19059}
19060
19061#[allow(clippy::too_many_arguments)]
19070fn fast_conv_enabled() -> bool {
19076 use std::sync::OnceLock;
19077 static FAST_CONV: OnceLock<bool> = OnceLock::new();
19078 *FAST_CONV.get_or_init(|| {
19079 matches!(
19080 rlx_ir::env::var("RLX_FAST_CONV").as_deref(),
19081 Some("1") | Some("on") | Some("true") | Some("yes")
19082 )
19083 })
19084}
19085
19086#[allow(clippy::too_many_arguments)]
19091fn conv2d_forward_naive(
19092 inp: &[f32],
19093 wt: &[f32],
19094 out: &mut [f32],
19095 n: usize,
19096 c_in: usize,
19097 h: usize,
19098 w: usize,
19099 c_out: usize,
19100 h_out: usize,
19101 w_out: usize,
19102 kh: usize,
19103 kw: usize,
19104 sh: usize,
19105 sw: usize,
19106 ph: usize,
19107 pw: usize,
19108 dh: usize,
19109 dw: usize,
19110 groups: usize,
19111) {
19112 let c_in_per_g = c_in / groups;
19113 let c_out_per_g = c_out / groups;
19114 for ni in 0..n {
19115 for co in 0..c_out {
19116 let g = co / c_out_per_g;
19117 let ci_start = g * c_in_per_g;
19118 for ho in 0..h_out {
19119 for wo in 0..w_out {
19120 let mut acc = 0f32;
19121 for ci_off in 0..c_in_per_g {
19122 let ci = ci_start + ci_off;
19123 let in_chan = ((ni * c_in) + ci) * h * w;
19124 let wt_chan = ((co * c_in_per_g) + ci_off) * kh * kw;
19125 for ki in 0..kh {
19126 for kj in 0..kw {
19127 let hi = ho * sh + ki * dh;
19128 let wi = wo * sw + kj * dw;
19129 if hi < ph || wi < pw {
19130 continue;
19131 }
19132 let hi = hi - ph;
19133 let wi = wi - pw;
19134 if hi >= h || wi >= w {
19135 continue;
19136 }
19137 acc += inp[in_chan + hi * w + wi] * wt[wt_chan + ki * kw + kj];
19138 }
19139 }
19140 }
19141 out[((ni * c_out) + co) * h_out * w_out + ho * w_out + wo] = acc;
19142 }
19143 }
19144 }
19145 }
19146}
19147
19148fn winograd_enabled() -> bool {
19151 use std::sync::OnceLock;
19152 static W: OnceLock<bool> = OnceLock::new();
19153 *W.get_or_init(|| {
19154 matches!(
19155 rlx_ir::env::var("RLX_WINOGRAD").as_deref(),
19156 Some("1") | Some("on") | Some("true") | Some("yes")
19157 )
19158 })
19159}
19160
19161fn direct_conv_enabled() -> bool {
19164 use std::sync::OnceLock;
19165 static D: OnceLock<bool> = OnceLock::new();
19166 *D.get_or_init(|| {
19167 matches!(
19168 rlx_ir::env::var("RLX_DIRECT_CONV").as_deref(),
19169 Some("1") | Some("on") | Some("true") | Some("yes")
19170 )
19171 })
19172}
19173
19174#[inline]
19176fn winograd_filter_transform(g: &[f32]) -> [f32; 16] {
19177 let mut tmp = [0f32; 12]; for j in 0..3 {
19179 let (g0, g1, g2) = (g[j], g[3 + j], g[6 + j]);
19180 tmp[j] = g0;
19181 tmp[3 + j] = 0.5 * (g0 + g1 + g2);
19182 tmp[6 + j] = 0.5 * (g0 - g1 + g2);
19183 tmp[9 + j] = g2;
19184 }
19185 let mut u = [0f32; 16];
19186 for i in 0..4 {
19187 let (t0, t1, t2) = (tmp[i * 3], tmp[i * 3 + 1], tmp[i * 3 + 2]);
19188 u[i * 4] = t0;
19189 u[i * 4 + 1] = 0.5 * (t0 + t1 + t2);
19190 u[i * 4 + 2] = 0.5 * (t0 - t1 + t2);
19191 u[i * 4 + 3] = t2;
19192 }
19193 u
19194}
19195
19196#[inline]
19198fn winograd_input_transform(d: &[f32; 16]) -> [f32; 16] {
19199 let mut t = [0f32; 16];
19200 for j in 0..4 {
19201 let (d0, d1, d2, d3) = (d[j], d[4 + j], d[8 + j], d[12 + j]);
19202 t[j] = d0 - d2;
19203 t[4 + j] = d1 + d2;
19204 t[8 + j] = d2 - d1;
19205 t[12 + j] = d1 - d3;
19206 }
19207 let mut v = [0f32; 16];
19208 for i in 0..4 {
19209 let (a0, a1, a2, a3) = (t[i * 4], t[i * 4 + 1], t[i * 4 + 2], t[i * 4 + 3]);
19210 v[i * 4] = a0 - a2;
19211 v[i * 4 + 1] = a1 + a2;
19212 v[i * 4 + 2] = a2 - a1;
19213 v[i * 4 + 3] = a1 - a3;
19214 }
19215 v
19216}
19217
19218#[inline]
19220fn winograd_output_transform(m: &[f32; 16]) -> [f32; 4] {
19221 let mut s = [0f32; 8];
19222 for j in 0..4 {
19223 let (m0, m1, m2, m3) = (m[j], m[4 + j], m[8 + j], m[12 + j]);
19224 s[j] = m0 + m1 + m2;
19225 s[4 + j] = m1 - m2 - m3;
19226 }
19227 let mut y = [0f32; 4];
19228 for i in 0..2 {
19229 let (a0, a1, a2, a3) = (s[i * 4], s[i * 4 + 1], s[i * 4 + 2], s[i * 4 + 3]);
19230 y[i * 2] = a0 + a1 + a2;
19231 y[i * 2 + 1] = a1 - a2 - a3;
19232 }
19233 y
19234}
19235
19236#[allow(clippy::too_many_arguments)]
19242fn conv2d_forward_winograd(
19243 inp: &[f32],
19244 wt: &[f32],
19245 out: &mut [f32],
19246 n: usize,
19247 c_in: usize,
19248 h: usize,
19249 w: usize,
19250 c_out: usize,
19251 h_out: usize,
19252 w_out: usize,
19253) {
19254 let th = h_out.div_ceil(2);
19255 let tw = w_out.div_ceil(2);
19256 let tiles_per = th * tw;
19257 let nt = n * tiles_per;
19258 if nt == 0 {
19259 return;
19260 }
19261
19262 let mut u = vec![0f32; 16 * c_out * c_in];
19264 for co in 0..c_out {
19265 for ci in 0..c_in {
19266 let uu = winograd_filter_transform(&wt[(co * c_in + ci) * 9..(co * c_in + ci) * 9 + 9]);
19267 for (p, &val) in uu.iter().enumerate() {
19268 u[p * c_out * c_in + co * c_in + ci] = val;
19269 }
19270 }
19271 }
19272
19273 let mut v = vec![0f32; 16 * c_in * nt];
19275 let v_addr = v.as_mut_ptr() as usize;
19276 let in_xform = |tile: usize| {
19277 let ni = tile / tiles_per;
19278 let rem = tile % tiles_per;
19279 let (h0, w0) = (2 * (rem / tw), 2 * (rem % tw));
19280 for ci in 0..c_in {
19281 let mut d = [0f32; 16];
19282 for di in 0..4 {
19283 let hh = h0 + di;
19284 if hh >= h {
19285 continue;
19286 }
19287 let base = ((ni * c_in + ci) * h + hh) * w;
19288 for dj in 0..4 {
19289 let ww = w0 + dj;
19290 if ww < w {
19291 d[di * 4 + dj] = inp[base + ww];
19292 }
19293 }
19294 }
19295 let vv = winograd_input_transform(&d);
19296 for (p, &val) in vv.iter().enumerate() {
19297 unsafe {
19298 *((v_addr as *mut f32).add(p * c_in * nt + ci * nt + tile)) = val;
19299 }
19300 }
19301 }
19302 };
19303 if fast_conv_enabled() && crate::pool::should_parallelize(nt * c_in * 16) {
19304 crate::pool::par_for(nt, crate::pool::outer_chunk(nt), &|off, cnt| {
19305 for t in off..off + cnt {
19306 in_xform(t);
19307 }
19308 });
19309 } else {
19310 for t in 0..nt {
19311 in_xform(t);
19312 }
19313 }
19314
19315 let mut m = vec![0f32; 16 * c_out * nt];
19317 for p in 0..16 {
19318 crate::blas::sgemm(
19319 &u[p * c_out * c_in..(p + 1) * c_out * c_in],
19320 &v[p * c_in * nt..(p + 1) * c_in * nt],
19321 &mut m[p * c_out * nt..(p + 1) * c_out * nt],
19322 c_out,
19323 c_in,
19324 nt,
19325 );
19326 }
19327
19328 let out_addr = out.as_mut_ptr() as usize;
19330 let out_xform = |tile: usize| {
19331 let ni = tile / tiles_per;
19332 let rem = tile % tiles_per;
19333 let (ho0, wo0) = (2 * (rem / tw), 2 * (rem % tw));
19334 for co in 0..c_out {
19335 let mut mm = [0f32; 16];
19336 for (p, slot) in mm.iter_mut().enumerate() {
19337 *slot = m[p * c_out * nt + co * nt + tile];
19338 }
19339 let y = winograd_output_transform(&mm);
19340 for yi in 0..2 {
19341 let oh = ho0 + yi;
19342 if oh >= h_out {
19343 continue;
19344 }
19345 for yj in 0..2 {
19346 let ow = wo0 + yj;
19347 if ow < w_out {
19348 unsafe {
19349 *((out_addr as *mut f32)
19350 .add(((ni * c_out + co) * h_out + oh) * w_out + ow)) =
19351 y[yi * 2 + yj];
19352 }
19353 }
19354 }
19355 }
19356 }
19357 };
19358 if fast_conv_enabled() && crate::pool::should_parallelize(nt * c_out * 16) {
19359 crate::pool::par_for(nt, crate::pool::outer_chunk(nt), &|off, cnt| {
19360 for t in off..off + cnt {
19361 out_xform(t);
19362 }
19363 });
19364 } else {
19365 for t in 0..nt {
19366 out_xform(t);
19367 }
19368 }
19369}
19370
19371#[allow(clippy::too_many_arguments)]
19379fn conv2d_forward_direct(
19380 inp: &[f32],
19381 wt: &[f32],
19382 out: &mut [f32],
19383 n: usize,
19384 c_in: usize,
19385 h: usize,
19386 w: usize,
19387 c_out: usize,
19388 h_out: usize,
19389 w_out: usize,
19390 kh: usize,
19391 kw: usize,
19392 groups: usize,
19393) {
19394 let c_in_per_g = c_in / groups;
19395 let c_out_per_g = c_out / groups;
19396 let out_plane = h_out * w_out;
19397 let in_plane = h * w;
19398 let out_addr = out.as_mut_ptr() as usize;
19399 let compute = |nco: usize| {
19400 let ni = nco / c_out;
19401 let co = nco % c_out;
19402 let ci_start = (co / c_out_per_g) * c_in_per_g;
19403 let out_base = (ni * c_out + co) * out_plane;
19404 let op = unsafe {
19405 std::slice::from_raw_parts_mut((out_addr as *mut f32).add(out_base), out_plane)
19406 };
19407 for v in op.iter_mut() {
19408 *v = 0.0;
19409 }
19410 for ci_off in 0..c_in_per_g {
19411 let in_base = (ni * c_in + ci_start + ci_off) * in_plane;
19412 let wt_base = (co * c_in_per_g + ci_off) * kh * kw;
19413 for ki in 0..kh {
19414 for kj in 0..kw {
19415 let wv = wt[wt_base + ki * kw + kj];
19416 for ho in 0..h_out {
19417 let in_row = in_base + (ho + ki) * w + kj;
19418 let dst = &mut op[ho * w_out..ho * w_out + w_out];
19419 let src = &inp[in_row..in_row + w_out];
19420 for wo in 0..w_out {
19421 dst[wo] += wv * src[wo];
19422 }
19423 }
19424 }
19425 }
19426 }
19427 };
19428 if fast_conv_enabled() && crate::pool::should_parallelize(n * c_out * out_plane) {
19429 crate::pool::par_for(
19430 n * c_out,
19431 crate::pool::outer_chunk(n * c_out),
19432 &|off, cnt| {
19433 for nco in off..off + cnt {
19434 compute(nco);
19435 }
19436 },
19437 );
19438 } else {
19439 for nco in 0..n * c_out {
19440 compute(nco);
19441 }
19442 }
19443}
19444
19445#[allow(clippy::too_many_arguments)]
19457fn conv2d_forward_im2col(
19458 inp: &[f32],
19459 wt: &[f32],
19460 out: &mut [f32],
19461 n: usize,
19462 c_in: usize,
19463 h: usize,
19464 w: usize,
19465 c_out: usize,
19466 h_out: usize,
19467 w_out: usize,
19468 kh: usize,
19469 kw: usize,
19470 sh: usize,
19471 sw: usize,
19472 ph: usize,
19473 pw: usize,
19474 dh: usize,
19475 dw: usize,
19476 groups: usize,
19477) {
19478 let c_in_per_g = c_in / groups;
19479 let c_out_per_g = c_out / groups;
19480 let k_dim = c_in_per_g * kh * kw; let p_dim = h_out * w_out; let x_stride_n = c_in * h * w;
19483 let x_stride_g = c_in_per_g * h * w;
19484 let out_stride_n = c_out * h_out * w_out;
19485 let out_stride_g = c_out_per_g * p_dim;
19486 let w_stride_g = c_out_per_g * k_dim;
19487
19488 let out_addr = out.as_mut_ptr() as usize;
19491 crate::pool::par_for(n, 1, &|off, cnt| {
19492 let mut col = vec![0f32; k_dim * p_dim];
19493 for ni in off..off + cnt {
19494 for g in 0..groups {
19495 let x_off = ni * x_stride_n + g * x_stride_g;
19496 im2col(
19497 &inp[x_off..x_off + x_stride_g],
19498 &mut col,
19499 c_in_per_g,
19500 h,
19501 w,
19502 h_out,
19503 w_out,
19504 kh,
19505 kw,
19506 sh,
19507 sw,
19508 ph,
19509 pw,
19510 dh,
19511 dw,
19512 );
19513 let w_off = g * w_stride_g;
19514 let o_off = ni * out_stride_n + g * out_stride_g;
19515 let out_g = unsafe {
19516 std::slice::from_raw_parts_mut((out_addr as *mut f32).add(o_off), out_stride_g)
19517 };
19518 crate::blas::sgemm(
19519 &wt[w_off..w_off + w_stride_g],
19520 &col,
19521 out_g,
19522 c_out_per_g,
19523 k_dim,
19524 p_dim,
19525 );
19526 }
19527 }
19528 });
19529}
19530
19531fn im2col(
19532 x: &[f32],
19533 col: &mut [f32],
19534 c_in: usize,
19535 h: usize,
19536 w: usize,
19537 h_out: usize,
19538 w_out: usize,
19539 kh: usize,
19540 kw: usize,
19541 sh: usize,
19542 sw: usize,
19543 ph: usize,
19544 pw: usize,
19545 dh: usize,
19546 dw_dil: usize,
19547) {
19548 let n_dim = h_out * w_out;
19549 debug_assert_eq!(col.len(), c_in * kh * kw * n_dim);
19550 debug_assert_eq!(x.len(), c_in * h * w);
19551 let h_isz = h as isize;
19552 let w_isz = w as isize;
19553 let ph_isz = ph as isize;
19554 let pw_isz = pw as isize;
19555 for ci in 0..c_in {
19556 for ki in 0..kh {
19557 for kj in 0..kw {
19558 let row = ((ci * kh) + ki) * kw + kj;
19559 let row_off = row * n_dim;
19560 for ho in 0..h_out {
19561 let hi = (ho * sh + ki * dh) as isize - ph_isz;
19562 if hi < 0 || hi >= h_isz {
19563 for wo in 0..w_out {
19564 col[row_off + ho * w_out + wo] = 0.0;
19565 }
19566 continue;
19567 }
19568 let hi = hi as usize;
19569 let in_row_off = (ci * h + hi) * w;
19570 for wo in 0..w_out {
19571 let wi = (wo * sw + kj * dw_dil) as isize - pw_isz;
19572 col[row_off + ho * w_out + wo] = if wi < 0 || wi >= w_isz {
19573 0.0
19574 } else {
19575 x[in_row_off + wi as usize]
19576 };
19577 }
19578 }
19579 }
19580 }
19581 }
19582}
19583
19584#[allow(clippy::too_many_arguments)]
19591fn col2im(
19592 col: &[f32],
19593 x: &mut [f32],
19594 c_in: usize,
19595 h: usize,
19596 w: usize,
19597 h_out: usize,
19598 w_out: usize,
19599 kh: usize,
19600 kw: usize,
19601 sh: usize,
19602 sw: usize,
19603 ph: usize,
19604 pw: usize,
19605 dh: usize,
19606 dw_dil: usize,
19607) {
19608 let n_dim = h_out * w_out;
19609 debug_assert_eq!(col.len(), c_in * kh * kw * n_dim);
19610 debug_assert_eq!(x.len(), c_in * h * w);
19611 let h_isz = h as isize;
19612 let w_isz = w as isize;
19613 let ph_isz = ph as isize;
19614 let pw_isz = pw as isize;
19615 for ci in 0..c_in {
19616 for ki in 0..kh {
19617 for kj in 0..kw {
19618 let row = ((ci * kh) + ki) * kw + kj;
19619 let row_off = row * n_dim;
19620 for ho in 0..h_out {
19621 let hi = (ho * sh + ki * dh) as isize - ph_isz;
19622 if hi < 0 || hi >= h_isz {
19623 continue;
19624 }
19625 let hi = hi as usize;
19626 let in_row_off = (ci * h + hi) * w;
19627 for wo in 0..w_out {
19628 let wi = (wo * sw + kj * dw_dil) as isize - pw_isz;
19629 if wi < 0 || wi >= w_isz {
19630 continue;
19631 }
19632 x[in_row_off + wi as usize] += col[row_off + ho * w_out + wo];
19633 }
19634 }
19635 }
19636 }
19637 }
19638}
19639
19640fn quant_layout(shape: &rlx_ir::Shape, axis: Option<usize>) -> (usize, usize, usize) {
19650 match axis {
19651 None => (0, 1, shape.num_elements().unwrap_or(0).max(1)),
19652 Some(d) => {
19653 let chan_dim = shape.dim(d).unwrap_static();
19654 let inner: usize = (d + 1..shape.rank())
19655 .map(|i| shape.dim(i).unwrap_static())
19656 .product::<usize>()
19657 .max(1);
19658 (d, chan_dim, inner)
19659 }
19660 }
19661}
19662
19663fn activation_backward_kernel(
19664 act: rlx_ir::op::Activation,
19665 xs: &[f32],
19666 dys: &[f32],
19667 out: &mut [f32],
19668) {
19669 use rlx_ir::op::Activation;
19670 let n = xs.len();
19671 debug_assert_eq!(dys.len(), n);
19672 debug_assert_eq!(out.len(), n);
19673 match act {
19674 Activation::Relu => {
19675 for i in 0..n {
19676 out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
19677 }
19678 }
19679 Activation::Sigmoid => {
19680 for i in 0..n {
19681 let s = 1.0 / (1.0 + (-xs[i]).exp());
19682 out[i] = s * (1.0 - s) * dys[i];
19683 }
19684 }
19685 Activation::Tanh => {
19686 for i in 0..n {
19687 let t = xs[i].tanh();
19688 out[i] = (1.0 - t * t) * dys[i];
19689 }
19690 }
19691 Activation::Silu => {
19692 for i in 0..n {
19694 let s = 1.0 / (1.0 + (-xs[i]).exp());
19695 out[i] = s * (1.0 + xs[i] * (1.0 - s)) * dys[i];
19696 }
19697 }
19698 Activation::Gelu => {
19699 const INV_SQRT2: f32 = 0.707_106_77;
19702 const INV_SQRT_2PI: f32 = 0.398_942_3;
19703 for i in 0..n {
19704 let x = xs[i];
19705 let phi = 0.5 * (1.0 + erf_f32(x * INV_SQRT2));
19706 let pdf = INV_SQRT_2PI * (-(x * x) * 0.5).exp();
19707 out[i] = (phi + x * pdf) * dys[i];
19708 }
19709 }
19710 Activation::GeluApprox => {
19711 const C: f32 = 0.797_884_6; const A: f32 = 0.044_715;
19715 for i in 0..n {
19716 let x = xs[i];
19717 let inner = C * (x + A * x * x * x);
19718 let t = inner.tanh();
19719 let dinner = C * (1.0 + 3.0 * A * x * x);
19720 let d = 0.5 * (1.0 + t) + 0.5 * x * (1.0 - t * t) * dinner;
19721 out[i] = d * dys[i];
19722 }
19723 }
19724 Activation::Exp => {
19725 for i in 0..n {
19726 out[i] = xs[i].exp() * dys[i];
19727 }
19728 }
19729 Activation::Log => {
19730 for i in 0..n {
19731 out[i] = dys[i] / xs[i];
19732 }
19733 }
19734 Activation::Sqrt => {
19735 for i in 0..n {
19737 let s = xs[i].sqrt();
19738 out[i] = if s > 0.0 { 0.5 * dys[i] / s } else { 0.0 };
19739 }
19740 }
19741 Activation::Rsqrt => {
19742 for i in 0..n {
19744 let s = xs[i].sqrt();
19745 out[i] = if s > 0.0 {
19746 -0.5 * dys[i] / (xs[i] * s)
19747 } else {
19748 0.0
19749 };
19750 }
19751 }
19752 Activation::Neg => {
19753 for i in 0..n {
19754 out[i] = -dys[i];
19755 }
19756 }
19757 Activation::Abs => {
19758 for i in 0..n {
19760 let x = xs[i];
19761 let s = if x > 0.0 {
19762 1.0
19763 } else if x < 0.0 {
19764 -1.0
19765 } else {
19766 0.0
19767 };
19768 out[i] = s * dys[i];
19769 }
19770 }
19771 Activation::Round => {
19772 out.copy_from_slice(dys);
19777 }
19778 Activation::Sin => {
19779 for i in 0..n {
19781 out[i] = xs[i].cos() * dys[i];
19782 }
19783 }
19784 Activation::Cos => {
19785 for i in 0..n {
19786 out[i] = -xs[i].sin() * dys[i];
19787 }
19788 }
19789 Activation::Tan => {
19790 for i in 0..n {
19792 let t = xs[i].tan();
19793 out[i] = (1.0 + t * t) * dys[i];
19794 }
19795 }
19796 Activation::Atan => {
19797 for i in 0..n {
19799 let x = xs[i];
19800 out[i] = dys[i] / (1.0 + x * x);
19801 }
19802 }
19803 }
19804}
19805
19806fn activation_backward_kernel_f64(
19810 act: rlx_ir::op::Activation,
19811 xs: &[f64],
19812 dys: &[f64],
19813 out: &mut [f64],
19814) {
19815 use rlx_ir::op::Activation;
19816 let n = xs.len();
19817 debug_assert_eq!(dys.len(), n);
19818 debug_assert_eq!(out.len(), n);
19819 match act {
19820 Activation::Relu => {
19821 for i in 0..n {
19822 out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
19823 }
19824 }
19825 Activation::Sigmoid => {
19826 for i in 0..n {
19827 let s = 1.0 / (1.0 + (-xs[i]).exp());
19828 out[i] = s * (1.0 - s) * dys[i];
19829 }
19830 }
19831 Activation::Tanh => {
19832 for i in 0..n {
19833 let t = xs[i].tanh();
19834 out[i] = (1.0 - t * t) * dys[i];
19835 }
19836 }
19837 Activation::Silu => {
19838 for i in 0..n {
19839 let s = 1.0 / (1.0 + (-xs[i]).exp());
19840 out[i] = s * (1.0 + xs[i] * (1.0 - s)) * dys[i];
19841 }
19842 }
19843 Activation::Gelu | Activation::GeluApprox => {
19844 const INV_SQRT2: f64 = std::f64::consts::FRAC_1_SQRT_2;
19846 const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
19847 for i in 0..n {
19848 let x = xs[i];
19849 let phi = 0.5 * (1.0 + erf_f64(x * INV_SQRT2));
19850 let pdf = INV_SQRT_2PI * (-(x * x) * 0.5).exp();
19851 out[i] = (phi + x * pdf) * dys[i];
19852 }
19853 }
19854 Activation::Exp => {
19855 for i in 0..n {
19856 out[i] = xs[i].exp() * dys[i];
19857 }
19858 }
19859 Activation::Log => {
19860 for i in 0..n {
19861 out[i] = dys[i] / xs[i];
19862 }
19863 }
19864 Activation::Sqrt => {
19865 for i in 0..n {
19866 let s = xs[i].sqrt();
19867 out[i] = if s > 0.0 { 0.5 * dys[i] / s } else { 0.0 };
19868 }
19869 }
19870 Activation::Rsqrt => {
19871 for i in 0..n {
19872 let s = xs[i].sqrt();
19873 out[i] = if s > 0.0 {
19874 -0.5 * dys[i] / (xs[i] * s)
19875 } else {
19876 0.0
19877 };
19878 }
19879 }
19880 Activation::Neg => {
19881 for i in 0..n {
19882 out[i] = -dys[i];
19883 }
19884 }
19885 Activation::Abs => {
19886 for i in 0..n {
19887 let x = xs[i];
19888 let s = if x > 0.0 {
19889 1.0
19890 } else if x < 0.0 {
19891 -1.0
19892 } else {
19893 0.0
19894 };
19895 out[i] = s * dys[i];
19896 }
19897 }
19898 Activation::Round => {
19899 out.copy_from_slice(dys);
19900 }
19901 Activation::Sin => {
19902 for i in 0..n {
19903 out[i] = xs[i].cos() * dys[i];
19904 }
19905 }
19906 Activation::Cos => {
19907 for i in 0..n {
19908 out[i] = -xs[i].sin() * dys[i];
19909 }
19910 }
19911 Activation::Tan => {
19912 for i in 0..n {
19913 let t = xs[i].tan();
19914 out[i] = (1.0 + t * t) * dys[i];
19915 }
19916 }
19917 Activation::Atan => {
19918 for i in 0..n {
19919 let x = xs[i];
19920 out[i] = dys[i] / (1.0 + x * x);
19921 }
19922 }
19923 }
19924}
19925
19926#[inline(always)]
19931fn erf_f64(x: f64) -> f64 {
19932 let s = x.signum();
19933 let x = x.abs();
19934 let t = 1.0 / (1.0 + 0.327_591_1 * x);
19935 let y = 1.0
19936 - (((((1.061_405_43 * t - 1.453_152_03) * t) + 1.421_413_75) * t - 0.284_496_74) * t
19937 + 0.254_829_59)
19938 * t
19939 * (-x * x).exp();
19940 s * y
19941}
19942
19943#[inline(always)]
19946fn erf_f32(x: f32) -> f32 {
19947 let s = x.signum();
19948 let x = x.abs();
19949 let t = 1.0 / (1.0 + 0.327_591_1 * x);
19950 let y = 1.0
19951 - (((((1.061_405_4 * t - 1.453_152_1) * t) + 1.421_413_8) * t - 0.284_496_74) * t
19952 + 0.254_829_6)
19953 * t
19954 * (-x * x).exp();
19955 s * y
19956}
19957
19958fn narrow_thunk_closure(
19959 src: usize,
19960 dst: usize,
19961 outer: u32,
19962 src_stride: u32,
19963 dst_stride: u32,
19964 inner: u32,
19965 elem_bytes: u8,
19966) -> Arc<dyn Fn(*mut u8) + Send + Sync> {
19967 let (outer, ss, ds, inner, eb) = (
19968 outer as usize,
19969 src_stride as usize,
19970 dst_stride as usize,
19971 inner as usize,
19972 elem_bytes as usize,
19973 );
19974 let row_bytes = inner.saturating_mul(eb);
19975 let src_row_stride = ss.saturating_mul(eb);
19976 let dst_row_stride = ds.saturating_mul(eb);
19977 Arc::new(move |base: *mut u8| unsafe {
19978 if row_bytes == 0 || src == dst {
19979 return;
19980 }
19981 let arena_len = usize::MAX;
19983 for o in 0..outer {
19984 let s_off = src + o * src_row_stride;
19985 let d_off = dst + o * dst_row_stride;
19986 if s_off == d_off {
19987 continue;
19988 }
19989 if s_off.saturating_add(row_bytes) > arena_len
19990 || d_off.saturating_add(row_bytes) > arena_len
19991 {
19992 break;
19993 }
19994 std::ptr::copy_nonoverlapping(base.add(s_off), base.add(d_off), row_bytes);
19995 }
19996 })
19997}
19998
19999unsafe fn sl(offset: usize, base: *mut u8, len: usize) -> &'static [f32] {
20000 if offset == usize::MAX {
20001 return &[];
20002 }
20003 unsafe { std::slice::from_raw_parts(base.add(offset) as *const f32, len) }
20004}
20005
20006#[inline(always)]
20007unsafe fn sl_mut(offset: usize, base: *mut u8, len: usize) -> &'static mut [f32] {
20008 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut f32, len) }
20009}
20010
20011#[inline(always)]
20012unsafe fn sl_f64(offset: usize, base: *mut u8, len: usize) -> &'static [f64] {
20013 if offset == usize::MAX {
20014 return &[];
20015 }
20016 unsafe { std::slice::from_raw_parts(base.add(offset) as *const f64, len) }
20017}
20018
20019#[inline(always)]
20020unsafe fn sl_mut_f64(offset: usize, base: *mut u8, len: usize) -> &'static mut [f64] {
20021 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut f64, len) }
20022}
20023
20024#[inline(always)]
20029#[allow(dead_code)]
20030unsafe fn sl_i32(offset: usize, base: *mut u8, len: usize) -> &'static [i32] {
20031 if offset == usize::MAX {
20032 return &[];
20033 }
20034 unsafe { std::slice::from_raw_parts(base.add(offset) as *const i32, len) }
20035}
20036
20037#[inline(always)]
20038#[allow(dead_code)]
20039unsafe fn sl_mut_i32(offset: usize, base: *mut u8, len: usize) -> &'static mut [i32] {
20040 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut i32, len) }
20041}
20042
20043#[inline(always)]
20044unsafe fn sl_i64(offset: usize, base: *mut u8, len: usize) -> &'static [i64] {
20045 if offset == usize::MAX {
20046 return &[];
20047 }
20048 unsafe { std::slice::from_raw_parts(base.add(offset) as *const i64, len) }
20049}
20050
20051#[inline(always)]
20052unsafe fn sl_mut_i64(offset: usize, base: *mut u8, len: usize) -> &'static mut [i64] {
20053 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut i64, len) }
20054}
20055
20056fn transpose_walk_f64(inp: &[f64], out: &mut [f64], out_dims: &[u32], in_strides: &[u32]) {
20060 let rank = out_dims.len();
20061 let mut idx = vec![0u32; rank];
20062 for o in 0..out.len() {
20063 let mut src_off = 0usize;
20064 for d in 0..rank {
20065 src_off += idx[d] as usize * in_strides[d] as usize;
20066 }
20067 out[o] = inp[broadcast_src_index(src_off, inp.len())];
20068 for d in (0..rank).rev() {
20070 idx[d] += 1;
20071 if idx[d] < out_dims[d] {
20072 break;
20073 }
20074 idx[d] = 0;
20075 }
20076 }
20077}
20078
20079fn apply_activation_f64(inp: &[f64], out: &mut [f64], kind: Activation) {
20085 match kind {
20086 Activation::Neg => {
20087 for (o, &v) in out.iter_mut().zip(inp) {
20088 *o = -v;
20089 }
20090 }
20091 Activation::Exp => {
20092 for (o, &v) in out.iter_mut().zip(inp) {
20093 *o = v.exp();
20094 }
20095 }
20096 Activation::Log => {
20097 for (o, &v) in out.iter_mut().zip(inp) {
20098 *o = v.ln();
20099 }
20100 }
20101 Activation::Sqrt => {
20102 for (o, &v) in out.iter_mut().zip(inp) {
20103 *o = v.sqrt();
20104 }
20105 }
20106 Activation::Rsqrt => {
20107 for (o, &v) in out.iter_mut().zip(inp) {
20108 *o = 1.0 / v.sqrt();
20109 }
20110 }
20111 Activation::Abs => {
20112 for (o, &v) in out.iter_mut().zip(inp) {
20113 *o = v.abs();
20114 }
20115 }
20116 Activation::Tanh => {
20117 for (o, &v) in out.iter_mut().zip(inp) {
20118 *o = v.tanh();
20119 }
20120 }
20121 Activation::Sigmoid => {
20122 for (o, &v) in out.iter_mut().zip(inp) {
20123 *o = 1.0 / (1.0 + (-v).exp());
20124 }
20125 }
20126 Activation::Relu => {
20127 for (o, &v) in out.iter_mut().zip(inp) {
20128 *o = v.max(0.0);
20129 }
20130 }
20131 Activation::Round => {
20132 for (o, &v) in out.iter_mut().zip(inp) {
20133 *o = v.round_ties_even();
20134 }
20135 }
20136 Activation::Sin => {
20137 for (o, &v) in out.iter_mut().zip(inp) {
20138 *o = v.sin();
20139 }
20140 }
20141 Activation::Cos => {
20142 for (o, &v) in out.iter_mut().zip(inp) {
20143 *o = v.cos();
20144 }
20145 }
20146 Activation::Tan => {
20147 for (o, &v) in out.iter_mut().zip(inp) {
20148 *o = v.tan();
20149 }
20150 }
20151 Activation::Atan => {
20152 for (o, &v) in out.iter_mut().zip(inp) {
20153 *o = v.atan();
20154 }
20155 }
20156 Activation::Gelu | Activation::GeluApprox | Activation::Silu => {
20157 panic!(
20158 "apply_activation_f64: {kind:?} not yet implemented at f64. \
20159 Add when a workload needs it."
20160 );
20161 }
20162 }
20163}
20164
20165#[inline]
20166fn binary_op_f64(op: BinaryOp, a: f64, b: f64) -> f64 {
20167 match op {
20168 BinaryOp::Add => a + b,
20169 BinaryOp::Sub => a - b,
20170 BinaryOp::Mul => a * b,
20171 BinaryOp::Div => a / b,
20172 BinaryOp::Max => a.max(b),
20173 BinaryOp::Min => a.min(b),
20174 BinaryOp::Pow => a.powf(b),
20175 }
20176}
20177
20178fn reduce_sum_f64(inp: &[f64], out: &mut [f64], outer: usize, reduced: usize, inner: usize) {
20181 for o in 0..outer {
20182 for n in 0..inner {
20183 let mut acc = 0.0_f64;
20184 for r in 0..reduced {
20185 acc += inp[o * reduced * inner + r * inner + n];
20186 }
20187 out[o * inner + n] = acc;
20188 }
20189 }
20190}
20191
20192pub unsafe fn fill_rng_normal_arena(
20198 dst_off: usize,
20199 len: usize,
20200 mean: f32,
20201 scale: f32,
20202 key: u64,
20203 op_seed: Option<f32>,
20204 opts: rlx_ir::RngOptions,
20205 arena: *mut u8,
20206) {
20207 if len == 0 {
20208 return;
20209 }
20210 unsafe {
20211 let out = std::slice::from_raw_parts_mut((arena.add(dst_off)) as *mut f32, len);
20212 rlx_ir::fill_normal_like(out, mean, scale, opts, key, op_seed);
20213 }
20214}
20215
20216pub unsafe fn fill_rng_uniform_arena(
20217 dst_off: usize,
20218 len: usize,
20219 low: f32,
20220 high: f32,
20221 key: u64,
20222 op_seed: Option<f32>,
20223 opts: rlx_ir::RngOptions,
20224 arena: *mut u8,
20225) {
20226 if len == 0 {
20227 return;
20228 }
20229 unsafe {
20230 let out = std::slice::from_raw_parts_mut((arena.add(dst_off)) as *mut f32, len);
20231 rlx_ir::fill_uniform_like(out, low, high, opts, key, op_seed);
20232 }
20233}
20234
20235#[cfg(test)]
20236mod tests {
20237 use super::*;
20238 use rlx_ir::*;
20239
20240 #[test]
20245 fn conv2d_im2col_matches_naive() {
20246 let cases = [
20248 (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), ];
20255 for (idx, &(n, c_in, h, w, c_out, kh, kw, sh, sw, ph, pw, dh, dw, groups)) in
20256 cases.iter().enumerate()
20257 {
20258 let c_in_per_g = c_in / groups;
20259 let h_out = (h + 2 * ph - dh * (kh - 1) - 1) / sh + 1;
20260 let w_out = (w + 2 * pw - dw * (kw - 1) - 1) / sw + 1;
20261 let mut s: u32 = 0x9e37_79b9 ^ (idx as u32 + 1);
20263 let mut rand = || {
20264 s ^= s << 13;
20265 s ^= s >> 17;
20266 s ^= s << 5;
20267 (s as f32 / u32::MAX as f32) - 0.5
20268 };
20269 let inp: Vec<f32> = (0..n * c_in * h * w).map(|_| rand()).collect();
20270 let wt: Vec<f32> = (0..c_out * c_in_per_g * kh * kw).map(|_| rand()).collect();
20271 let mut out_ref = vec![0f32; n * c_out * h_out * w_out];
20272 let mut out_fast = vec![0f32; n * c_out * h_out * w_out];
20273
20274 conv2d_forward_naive(
20275 &inp,
20276 &wt,
20277 &mut out_ref,
20278 n,
20279 c_in,
20280 h,
20281 w,
20282 c_out,
20283 h_out,
20284 w_out,
20285 kh,
20286 kw,
20287 sh,
20288 sw,
20289 ph,
20290 pw,
20291 dh,
20292 dw,
20293 groups,
20294 );
20295 conv2d_forward_im2col(
20296 &inp,
20297 &wt,
20298 &mut out_fast,
20299 n,
20300 c_in,
20301 h,
20302 w,
20303 c_out,
20304 h_out,
20305 w_out,
20306 kh,
20307 kw,
20308 sh,
20309 sw,
20310 ph,
20311 pw,
20312 dh,
20313 dw,
20314 groups,
20315 );
20316
20317 let max_abs = out_ref
20318 .iter()
20319 .zip(&out_fast)
20320 .map(|(a, b)| (a - b).abs())
20321 .fold(0f32, f32::max);
20322 assert!(
20323 max_abs < 1e-3,
20324 "case {idx}: im2col vs naive max abs diff {max_abs}"
20325 );
20326 }
20327 }
20328
20329 #[test]
20332 fn conv2d_direct_matches_naive() {
20333 let cases = [
20335 (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), ];
20341 for (idx, &(n, c_in, h, w, c_out, kh, kw, groups)) in cases.iter().enumerate() {
20342 let h_out = h - kh + 1;
20343 let w_out = w - kw + 1;
20344 let c_in_per_g = c_in / groups;
20345 let mut s: u32 = 0xfeed_1234 ^ (idx as u32 + 1);
20346 let mut rand = || {
20347 s ^= s << 13;
20348 s ^= s >> 17;
20349 s ^= s << 5;
20350 (s as f32 / u32::MAX as f32) - 0.5
20351 };
20352 let inp: Vec<f32> = (0..n * c_in * h * w).map(|_| rand()).collect();
20353 let wt: Vec<f32> = (0..c_out * c_in_per_g * kh * kw).map(|_| rand()).collect();
20354 let mut r = vec![0f32; n * c_out * h_out * w_out];
20355 let mut d = vec![0f32; n * c_out * h_out * w_out];
20356 conv2d_forward_naive(
20357 &inp, &wt, &mut r, n, c_in, h, w, c_out, h_out, w_out, kh, kw, 1, 1, 0, 0, 1, 1,
20358 groups,
20359 );
20360 conv2d_forward_direct(
20361 &inp, &wt, &mut d, n, c_in, h, w, c_out, h_out, w_out, kh, kw, groups,
20362 );
20363 let mx = r
20364 .iter()
20365 .zip(&d)
20366 .map(|(a, b)| (a - b).abs())
20367 .fold(0f32, f32::max);
20368 assert!(mx < 1e-4, "case {idx}: direct vs naive max abs diff {mx}");
20369 }
20370 }
20371
20372 #[test]
20376 fn conv2d_winograd_matches_naive() {
20377 let cases = [
20380 (1, 1, 28, 28, 8), (4, 8, 13, 13, 16), (2, 3, 9, 9, 5), (1, 4, 8, 8, 4), ];
20385 for (idx, &(n, c_in, h, w, c_out)) in cases.iter().enumerate() {
20386 let h_out = h - 2;
20387 let w_out = w - 2;
20388 let mut s: u32 = 0x1234_5678 ^ (idx as u32 + 1);
20389 let mut rand = || {
20390 s ^= s << 13;
20391 s ^= s >> 17;
20392 s ^= s << 5;
20393 (s as f32 / u32::MAX as f32) - 0.5
20394 };
20395 let inp: Vec<f32> = (0..n * c_in * h * w).map(|_| rand()).collect();
20396 let wt: Vec<f32> = (0..c_out * c_in * 9).map(|_| rand()).collect();
20397 let mut out_ref = vec![0f32; n * c_out * h_out * w_out];
20398 let mut out_win = vec![0f32; n * c_out * h_out * w_out];
20399 conv2d_forward_naive(
20400 &inp,
20401 &wt,
20402 &mut out_ref,
20403 n,
20404 c_in,
20405 h,
20406 w,
20407 c_out,
20408 h_out,
20409 w_out,
20410 3,
20411 3,
20412 1,
20413 1,
20414 0,
20415 0,
20416 1,
20417 1,
20418 1,
20419 );
20420 conv2d_forward_winograd(&inp, &wt, &mut out_win, n, c_in, h, w, c_out, h_out, w_out);
20421 let max_abs = out_ref
20422 .iter()
20423 .zip(&out_win)
20424 .map(|(a, b)| (a - b).abs())
20425 .fold(0f32, f32::max);
20426 assert!(
20427 max_abs < 1e-3,
20428 "case {idx}: winograd vs naive max abs diff {max_abs}"
20429 );
20430 }
20431 }
20432
20433 #[test]
20439 fn narrow_rope_fuses_in_unfused_path() {
20440 let f = DType::F32;
20441 let mut g = Graph::new("nr_fuse");
20442 let qkv = g.input("qkv", Shape::new(&[16, 8, 192], f)); let cos = g.input("cos", Shape::new(&[16], f));
20445 let sin = g.input("sin", Shape::new(&[16], f));
20446 let q = g.narrow_(qkv, 2, 0, 64);
20448 let q_rope = g.rope(q, cos, sin, 16);
20449 g.set_outputs(vec![q_rope]);
20450
20451 let plan = rlx_opt::memory::plan_memory(&g);
20452 let arena = crate::arena::Arena::from_plan(plan);
20453 let sched = compile_thunks(&g, &arena);
20454
20455 let mut narrow_count = 0;
20456 let mut rope_with_stride: Option<u32> = None;
20457 for t in &sched.thunks {
20458 match t {
20459 Thunk::Narrow { .. } => narrow_count += 1,
20460 Thunk::Rope { src_row_stride, .. } => rope_with_stride = Some(*src_row_stride),
20461 _ => {}
20462 }
20463 }
20464 assert_eq!(
20467 narrow_count, 0,
20468 "Narrow→Rope fusion should leave zero Narrow thunks; saw {narrow_count}"
20469 );
20470 assert_eq!(
20471 rope_with_stride,
20472 Some(192),
20473 "Rope's src_row_stride should be 192 (parent qkv axis), saw {rope_with_stride:?}"
20474 );
20475 }
20476
20477 #[test]
20480 fn ssm_selective_scan_matches_reference() {
20481 use rlx_ir::Philox4x32;
20482 let bch = 1usize;
20483 let s = 4usize;
20484 let h = 3usize;
20485 let n = 2usize;
20486
20487 let mut rng = Philox4x32::new(13);
20488 let mut x = vec![0f32; bch * s * h];
20489 rng.fill_normal(&mut x);
20490 let mut delta = vec![0f32; bch * s * h];
20491 for v in delta.iter_mut() {
20493 *v = (rng.next_f32() - 0.5) * 0.1;
20494 }
20495 let mut a = vec![0f32; h * n];
20496 for v in a.iter_mut() {
20497 *v = -(rng.next_f32() * 0.5 + 0.1);
20498 } let mut b = vec![0f32; bch * s * n];
20500 rng.fill_normal(&mut b);
20501 let mut c = vec![0f32; bch * s * n];
20502 rng.fill_normal(&mut c);
20503
20504 let mut expected = vec![0f32; bch * s * h];
20506 for bi in 0..bch {
20507 let mut state = vec![0f32; h * n];
20508 for si in 0..s {
20509 for ci in 0..h {
20510 let d = delta[bi * s * h + si * h + ci];
20511 let xv = x[bi * s * h + si * h + ci];
20512 let mut acc = 0f32;
20513 for ni in 0..n {
20514 let da = (d * a[ci * n + ni]).exp();
20515 state[ci * n + ni] =
20516 da * state[ci * n + ni] + d * b[bi * s * n + si * n + ni] * xv;
20517 acc += c[bi * s * n + si * n + ni] * state[ci * n + ni];
20518 }
20519 expected[bi * s * h + si * h + ci] = acc;
20520 }
20521 }
20522 }
20523
20524 let f = DType::F32;
20526 let mut g = Graph::new("ssm");
20527 let xn = g.input("x", Shape::new(&[bch, s, h], f));
20528 let dn = g.input("delta", Shape::new(&[bch, s, h], f));
20529 let an = g.param("a", Shape::new(&[h, n], f));
20530 let bn = g.param("b", Shape::new(&[bch, s, n], f));
20531 let cn = g.param("c", Shape::new(&[bch, s, n], f));
20532 let yn = g.selective_scan(xn, dn, an, bn, cn, n, Shape::new(&[bch, s, h], f));
20533 g.set_outputs(vec![yn]);
20534
20535 let plan = rlx_opt::memory::plan_memory(&g);
20536 let mut arena = crate::arena::Arena::from_plan(plan);
20537 let sched = compile_thunks(&g, &arena);
20538
20539 let xn_off = arena.byte_offset(xn);
20540 let dn_off = arena.byte_offset(dn);
20541 let an_off = arena.byte_offset(an);
20542 let bn_off = arena.byte_offset(bn);
20543 let cn_off = arena.byte_offset(cn);
20544 let yn_off = arena.byte_offset(yn);
20545 let buf = arena.raw_buf_mut();
20546 unsafe {
20547 let copy = |dst: *mut f32, data: &[f32]| {
20548 for (i, &v) in data.iter().enumerate() {
20549 *dst.add(i) = v;
20550 }
20551 };
20552 copy(buf.as_mut_ptr().add(xn_off) as *mut f32, &x);
20553 copy(buf.as_mut_ptr().add(dn_off) as *mut f32, &delta);
20554 copy(buf.as_mut_ptr().add(an_off) as *mut f32, &a);
20555 copy(buf.as_mut_ptr().add(bn_off) as *mut f32, &b);
20556 copy(buf.as_mut_ptr().add(cn_off) as *mut f32, &c);
20557 }
20558 execute_thunks(&sched, arena.raw_buf_mut());
20559
20560 let actual: Vec<f32> = unsafe {
20561 let p = arena.raw_buf().as_ptr().add(yn_off) as *const f32;
20562 (0..bch * s * h).map(|i| *p.add(i)).collect()
20563 };
20564
20565 for (i, (e, a)) in expected.iter().zip(&actual).enumerate() {
20566 assert!(
20567 (e - a).abs() < 1e-3,
20568 "mismatch at {i}: expected {e}, got {a}"
20569 );
20570 }
20571 }
20572
20573 #[test]
20576 fn conv_1x1_fast_path_matches_scalar() {
20577 use rlx_ir::Philox4x32;
20578 let n = 2usize;
20580 let c_in = 4usize;
20581 let h = 3usize;
20582 let w = 3usize;
20583 let c_out = 5usize;
20584 let mut rng = Philox4x32::new(31);
20585 let mut x = vec![0f32; n * c_in * h * w];
20586 rng.fill_normal(&mut x);
20587 let mut weight = vec![0f32; c_out * c_in];
20588 rng.fill_normal(&mut weight);
20589
20590 let mut expected = vec![0f32; n * c_out * h * w];
20593 for ni in 0..n {
20594 for co in 0..c_out {
20595 for hi in 0..h {
20596 for wi in 0..w {
20597 let mut acc = 0f32;
20598 for ci in 0..c_in {
20599 acc += weight[co * c_in + ci]
20600 * x[((ni * c_in) + ci) * h * w + hi * w + wi];
20601 }
20602 expected[((ni * c_out) + co) * h * w + hi * w + wi] = acc;
20603 }
20604 }
20605 }
20606 }
20607
20608 let f = DType::F32;
20610 let mut g = Graph::new("conv1x1");
20611 let xn = g.input("x", Shape::new(&[n, c_in, h, w], f));
20612 let wn = g.param("w", Shape::new(&[c_out, c_in, 1, 1], f));
20613 let cn = g.add_node(
20615 rlx_ir::Op::Conv {
20616 kernel_size: vec![1, 1],
20617 stride: vec![1, 1],
20618 padding: vec![0, 0],
20619 dilation: vec![1, 1],
20620 groups: 1,
20621 },
20622 vec![xn, wn],
20623 Shape::new(&[n, c_out, h, w], f),
20624 );
20625 g.set_outputs(vec![cn]);
20626
20627 let plan = rlx_opt::memory::plan_memory(&g);
20628 let mut arena = crate::arena::Arena::from_plan(plan);
20629 let sched = compile_thunks(&g, &arena);
20630
20631 let saw_fast = sched
20633 .thunks
20634 .iter()
20635 .any(|t| matches!(t, Thunk::Conv2D1x1 { .. }));
20636 let saw_slow = sched
20637 .thunks
20638 .iter()
20639 .any(|t| matches!(t, Thunk::Conv2D { .. }));
20640 assert!(saw_fast, "1×1 conv should emit Conv2D1x1");
20641 assert!(!saw_slow, "1×1 conv must not fall through to scalar Conv2D");
20642
20643 let xn_off = arena.byte_offset(xn);
20644 let wn_off = arena.byte_offset(wn);
20645 let cn_off = arena.byte_offset(cn);
20646 let buf = arena.raw_buf_mut();
20647 unsafe {
20648 let xp = buf.as_mut_ptr().add(xn_off) as *mut f32;
20649 for (i, &v) in x.iter().enumerate() {
20650 *xp.add(i) = v;
20651 }
20652 let wp = buf.as_mut_ptr().add(wn_off) as *mut f32;
20653 for (i, &v) in weight.iter().enumerate() {
20654 *wp.add(i) = v;
20655 }
20656 }
20657 execute_thunks(&sched, arena.raw_buf_mut());
20658
20659 let actual: Vec<f32> = unsafe {
20660 let p = arena.raw_buf().as_ptr().add(cn_off) as *const f32;
20661 (0..(n * c_out * h * w)).map(|i| *p.add(i)).collect()
20662 };
20663
20664 for (i, (e, a)) in expected.iter().zip(&actual).enumerate() {
20665 assert!(
20666 (e - a).abs() < 1e-3,
20667 "mismatch at {i}: expected {e}, got {a}"
20668 );
20669 }
20670 }
20671
20672 #[test]
20675 fn dequant_matmul_int8_sym_matches_reference() {
20676 use rlx_ir::Philox4x32;
20677 use rlx_ir::quant::QuantScheme;
20678
20679 let m = 3usize;
20680 let k = 8usize;
20681 let n = 4usize;
20682 let block_size = 4usize; let blocks_per_col = k / block_size;
20684
20685 let mut rng = Philox4x32::new(99);
20687 let mut x = vec![0f32; m * k];
20688 rng.fill_normal(&mut x);
20689 let w_q: Vec<i8> = (0..(k * n))
20690 .map(|i| ((i as i32 * 13 + 7) % 127 - 63) as i8)
20691 .collect();
20692 let scales: Vec<f32> = (0..(blocks_per_col * n))
20693 .map(|i| 0.01 + 0.001 * i as f32)
20694 .collect();
20695
20696 let mut w_f32 = vec![0f32; k * n];
20698 for p in 0..k {
20699 let block = p / block_size;
20700 for j in 0..n {
20701 let s = scales[block * n + j];
20702 w_f32[p * n + j] = w_q[p * n + j] as f32 * s;
20703 }
20704 }
20705 let mut expected = vec![0f32; m * n];
20706 for i in 0..m {
20707 for j in 0..n {
20708 let mut acc = 0f32;
20709 for p in 0..k {
20710 acc += x[i * k + p] * w_f32[p * n + j];
20711 }
20712 expected[i * n + j] = acc;
20713 }
20714 }
20715
20716 let f = DType::F32;
20718 let mut g = Graph::new("dq");
20719 let xn = g.input("x", Shape::new(&[m, k], f));
20720 let wn = g.param("w", Shape::new(&[k, n], DType::I8));
20721 let sn = g.param("scale", Shape::new(&[blocks_per_col, n], f));
20722 let zn = g.param("zp", Shape::new(&[blocks_per_col, n], f)); let dq = g.dequant_matmul(
20724 xn,
20725 wn,
20726 sn,
20727 zn,
20728 QuantScheme::Int8Block {
20729 block_size: block_size as u32,
20730 },
20731 Shape::new(&[m, n], f),
20732 );
20733 g.set_outputs(vec![dq]);
20734
20735 let plan = rlx_opt::memory::plan_memory(&g);
20736 let mut arena = crate::arena::Arena::from_plan(plan);
20737 let sched = compile_thunks(&g, &arena);
20738
20739 let xn_off = arena.byte_offset(xn);
20740 let wn_off = arena.byte_offset(wn);
20741 let sn_off = arena.byte_offset(sn);
20742 let zn_off = arena.byte_offset(zn);
20743 let dq_off = arena.byte_offset(dq);
20744 let buf = arena.raw_buf_mut();
20745 unsafe {
20746 let xp = buf.as_mut_ptr().add(xn_off) as *mut f32;
20748 for (i, &v) in x.iter().enumerate() {
20749 *xp.add(i) = v;
20750 }
20751 let sp = buf.as_mut_ptr().add(sn_off) as *mut f32;
20752 for (i, &v) in scales.iter().enumerate() {
20753 *sp.add(i) = v;
20754 }
20755 let zp = buf.as_mut_ptr().add(zn_off) as *mut f32;
20756 for i in 0..(blocks_per_col * n) {
20757 *zp.add(i) = 0.0;
20758 }
20759 let wp = buf.as_mut_ptr().add(wn_off) as *mut i8;
20761 for (i, &v) in w_q.iter().enumerate() {
20762 *wp.add(i) = v;
20763 }
20764 }
20765 execute_thunks(&sched, arena.raw_buf_mut());
20766
20767 let actual: Vec<f32> = unsafe {
20768 let p = arena.raw_buf().as_ptr().add(dq_off) as *const f32;
20769 (0..m * n).map(|i| *p.add(i)).collect()
20770 };
20771
20772 for (i, (e, a)) in expected.iter().zip(&actual).enumerate() {
20773 assert!(
20774 (e - a).abs() < 1e-3,
20775 "mismatch at {i}: expected {e}, got {a}"
20776 );
20777 }
20778 }
20779
20780 #[test]
20782 fn lora_matmul_matches_unfused_reference() {
20783 use rlx_ir::Philox4x32;
20784
20785 let m = 4usize;
20786 let k = 8usize;
20787 let n = 6usize;
20788 let r = 2usize;
20789 let scale = 0.5f32;
20790
20791 let mut rng = Philox4x32::new(42);
20793 let mut x = vec![0f32; m * k];
20794 rng.fill_normal(&mut x);
20795 let mut w = vec![0f32; k * n];
20796 rng.fill_normal(&mut w);
20797 let mut a = vec![0f32; k * r];
20798 rng.fill_normal(&mut a);
20799 let mut b = vec![0f32; r * n];
20800 rng.fill_normal(&mut b);
20801
20802 let naive = |a_buf: &[f32], b_buf: &[f32], rows: usize, inner: usize, cols: usize| {
20804 let mut o = vec![0f32; rows * cols];
20805 for i in 0..rows {
20806 for j in 0..cols {
20807 let mut acc = 0f32;
20808 for p in 0..inner {
20809 acc += a_buf[i * inner + p] * b_buf[p * cols + j];
20810 }
20811 o[i * cols + j] = acc;
20812 }
20813 }
20814 o
20815 };
20816 let xw = naive(&x, &w, m, k, n);
20817 let xa = naive(&x, &a, m, k, r);
20818 let xab = naive(&xa, &b, m, r, n);
20819 let mut expected = xw;
20820 for i in 0..(m * n) {
20821 expected[i] += scale * xab[i];
20822 }
20823
20824 let f = DType::F32;
20826 let mut g = Graph::new("lora");
20827 let xn = g.input("x", Shape::new(&[m, k], f));
20828 let wn = g.param("w", Shape::new(&[k, n], f));
20829 let an = g.param("a", Shape::new(&[k, r], f));
20830 let bn = g.param("b", Shape::new(&[r, n], f));
20831 let lm = g.lora_matmul(xn, wn, an, bn, scale, Shape::new(&[m, n], f));
20832 g.set_outputs(vec![lm]);
20833
20834 let plan = rlx_opt::memory::plan_memory(&g);
20835 let mut arena = crate::arena::Arena::from_plan(plan);
20836 let sched = compile_thunks(&g, &arena);
20837
20838 let xn_off = arena.byte_offset(xn);
20839 let wn_off = arena.byte_offset(wn);
20840 let an_off = arena.byte_offset(an);
20841 let bn_off = arena.byte_offset(bn);
20842 let lm_off = arena.byte_offset(lm);
20843 let buf = arena.raw_buf_mut();
20844 unsafe {
20845 let copy = |dst: *mut f32, data: &[f32]| {
20846 for (i, &v) in data.iter().enumerate() {
20847 *dst.add(i) = v;
20848 }
20849 };
20850 copy(buf.as_mut_ptr().add(xn_off) as *mut f32, &x);
20851 copy(buf.as_mut_ptr().add(wn_off) as *mut f32, &w);
20852 copy(buf.as_mut_ptr().add(an_off) as *mut f32, &a);
20853 copy(buf.as_mut_ptr().add(bn_off) as *mut f32, &b);
20854 }
20855 execute_thunks(&sched, arena.raw_buf_mut());
20856
20857 let actual: Vec<f32> = unsafe {
20858 let p = arena.raw_buf().as_ptr().add(lm_off) as *const f32;
20859 (0..m * n).map(|i| *p.add(i)).collect()
20860 };
20861
20862 for (i, (e, a)) in expected.iter().zip(&actual).enumerate() {
20863 assert!(
20864 (e - a).abs() < 1e-3,
20865 "mismatch at {i}: expected {e}, got {a}"
20866 );
20867 }
20868 }
20869
20870 #[test]
20872 fn sample_temperature_zero_is_argmax() {
20873 let f = DType::F32;
20876 let mut g = Graph::new("samp");
20877 let logits = g.input("logits", Shape::new(&[1, 8], f));
20878 let s = g.sample(logits, 0, 1.0, 1e-3, 42, Shape::new(&[1], f));
20879 g.set_outputs(vec![s]);
20880 let plan = rlx_opt::memory::plan_memory(&g);
20881 let mut arena = crate::arena::Arena::from_plan(plan);
20882 let sched = compile_thunks(&g, &arena);
20883
20884 let logits_off = arena.byte_offset(logits);
20885 let s_off = arena.byte_offset(s);
20886 let buf = arena.raw_buf_mut();
20887 unsafe {
20888 let p = buf.as_mut_ptr().add(logits_off) as *mut f32;
20889 let inputs = [0.1f32, 0.2, 0.3, 0.4, 0.5, 9.0, 0.7, 0.8];
20891 for (i, &v) in inputs.iter().enumerate() {
20892 *p.add(i) = v;
20893 }
20894 }
20895 execute_thunks(&sched, arena.raw_buf_mut());
20896
20897 let token = unsafe {
20898 let p = arena.raw_buf().as_ptr().add(s_off) as *const f32;
20899 *p as usize
20900 };
20901 assert_eq!(token, 5, "low-temp sampling should pick the argmax");
20902 }
20903
20904 #[test]
20905 fn sample_top_k_one_is_deterministic() {
20906 let f = DType::F32;
20908 let mut g = Graph::new("samp_k1");
20909 let logits = g.input("logits", Shape::new(&[1, 4], f));
20910 let s = g.sample(logits, 1, 1.0, 1.0, 7, Shape::new(&[1], f));
20911 g.set_outputs(vec![s]);
20912 let plan = rlx_opt::memory::plan_memory(&g);
20913 let mut arena = crate::arena::Arena::from_plan(plan);
20914 let sched = compile_thunks(&g, &arena);
20915
20916 let logits_off = arena.byte_offset(logits);
20917 let s_off = arena.byte_offset(s);
20918 let buf = arena.raw_buf_mut();
20919 unsafe {
20920 let p = buf.as_mut_ptr().add(logits_off) as *mut f32;
20921 let inputs = [0.1f32, 5.0, 0.3, 0.4]; for (i, &v) in inputs.iter().enumerate() {
20923 *p.add(i) = v;
20924 }
20925 }
20926 execute_thunks(&sched, arena.raw_buf_mut());
20927 let token = unsafe {
20928 let p = arena.raw_buf().as_ptr().add(s_off) as *const f32;
20929 *p as usize
20930 };
20931 assert_eq!(token, 1);
20932 }
20933
20934 #[test]
20936 fn cumsum_inclusive_matches_naive() {
20937 let f = DType::F32;
20938 let mut g = Graph::new("cumsum");
20939 let x = g.input("x", Shape::new(&[2, 4], f));
20940 let cs = g.cumsum(x, -1, false, Shape::new(&[2, 4], f));
20941 g.set_outputs(vec![cs]);
20942 let plan = rlx_opt::memory::plan_memory(&g);
20943 let mut arena = crate::arena::Arena::from_plan(plan);
20944 let sched = compile_thunks(&g, &arena);
20945
20946 let x_off = arena.byte_offset(x);
20948 let out_off = arena.byte_offset(cs);
20949 let buf = arena.raw_buf_mut();
20950 unsafe {
20951 let p = buf.as_mut_ptr().add(x_off) as *mut f32;
20952 let inputs = [1.0f32, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0];
20953 for (i, &v) in inputs.iter().enumerate() {
20954 *p.add(i) = v;
20955 }
20956 }
20957 execute_thunks(&sched, arena.raw_buf_mut());
20958
20959 let out: Vec<f32> = unsafe {
20960 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
20961 (0..8).map(|i| *p.add(i)).collect()
20962 };
20963 assert_eq!(out, vec![1.0, 3.0, 6.0, 10.0, 10.0, 30.0, 60.0, 100.0]);
20964 }
20965
20966 #[test]
20970 fn narrow_attention_fuses_in_unfused_path() {
20971 let f = DType::F32;
20972 let mut g = Graph::new("nattn_fuse");
20973 let qkv = g.input("qkv", Shape::new(&[8, 16, 192], f)); let mask = g.input("mask", Shape::new(&[8, 16], f));
20976 let q = g.narrow_(qkv, 2, 0, 64);
20977 let k = g.narrow_(qkv, 2, 64, 64);
20978 let v = g.narrow_(qkv, 2, 128, 64);
20979 let attn = g.attention(q, k, v, mask, 4, 16, Shape::new(&[8, 16, 64], f));
20980 g.set_outputs(vec![attn]);
20981
20982 let plan = rlx_opt::memory::plan_memory(&g);
20983 let arena = crate::arena::Arena::from_plan(plan);
20984 let sched = compile_thunks(&g, &arena);
20985
20986 let mut narrow_count = 0;
20987 let mut attn_strides: Option<(u32, u32, u32)> = None;
20988 for t in &sched.thunks {
20989 match t {
20990 Thunk::Narrow { .. } => narrow_count += 1,
20991 Thunk::Attention {
20992 q_row_stride,
20993 k_row_stride,
20994 v_row_stride,
20995 ..
20996 } => attn_strides = Some((*q_row_stride, *k_row_stride, *v_row_stride)),
20997 _ => {}
20998 }
20999 }
21000 assert_eq!(
21003 narrow_count, 0,
21004 "Narrow×3→Attention fusion should eliminate all 3 narrows; saw {narrow_count}"
21005 );
21006 assert_eq!(
21007 attn_strides,
21008 Some((192, 192, 192)),
21009 "Attention should walk Q/K/V with parent row stride 192"
21010 );
21011 }
21012
21013 #[test]
21019 fn fused_attn_block_respects_causal_mask() {
21020 let f = DType::F32;
21021 let (s, d, nh, dh) = (5usize, 8usize, 2usize, 4usize);
21022 let half = dh / 2;
21023
21024 let mut g = Graph::new("fused_causal");
21025 let hidden = g.input("hidden", Shape::new(&[s, d], f));
21026 let wqkv = g.input("wqkv", Shape::new(&[d, 3 * d], f));
21027 let wo = g.input("wo", Shape::new(&[d, d], f));
21028 let cos = g.input("cos", Shape::new(&[s, half], f));
21029 let sin = g.input("sin", Shape::new(&[s, half], f));
21030 let qkv = g.matmul(hidden, wqkv, Shape::new(&[s, 3 * d], f));
21031 let q = g.narrow_(qkv, 1, 0, d);
21032 let k = g.narrow_(qkv, 1, d, d);
21033 let v = g.narrow_(qkv, 1, 2 * d, d);
21034 let q3 = g.reshape(q, vec![1, s as i64, d as i64], Shape::new(&[1, s, d], f));
21035 let k3 = g.reshape(k, vec![1, s as i64, d as i64], Shape::new(&[1, s, d], f));
21036 let v3 = g.reshape(v, vec![1, s as i64, d as i64], Shape::new(&[1, s, d], f));
21037 let qr = g.rope(q3, cos, sin, dh);
21038 let kr = g.rope(k3, cos, sin, dh);
21039 let attn = g.attention_kind(
21040 qr,
21041 kr,
21042 v3,
21043 nh,
21044 dh,
21045 rlx_ir::op::MaskKind::Causal,
21046 Shape::new(&[1, s, d], f),
21047 );
21048 let a2 = g.reshape(attn, vec![s as i64, d as i64], Shape::new(&[s, d], f));
21049 let out = g.matmul(a2, wo, Shape::new(&[s, d], f));
21050 g.set_outputs(vec![out]);
21051
21052 let plan = rlx_opt::memory::plan_memory(&g);
21054 let arena = crate::arena::Arena::from_plan(plan);
21055 let sched = compile_thunks(&g, &arena);
21056 assert!(
21057 sched.thunks.iter().any(|t| matches!(
21058 t,
21059 Thunk::FusedAttnBlock {
21060 mask_kind: rlx_ir::op::MaskKind::Causal,
21061 ..
21062 }
21063 )),
21064 "expected a FusedAttnBlock carrying MaskKind::Causal"
21065 );
21066
21067 let wqkv_d: Vec<f32> = (0..d * 3 * d)
21068 .map(|i| ((i % 7) as f32 - 3.0) * 0.05)
21069 .collect();
21070 let wo_d: Vec<f32> = (0..d * d).map(|i| ((i % 5) as f32 - 2.0) * 0.05).collect();
21071 let mut cos_d = vec![0f32; s * half];
21072 let mut sin_d = vec![0f32; s * half];
21073 for p in 0..s {
21074 for i in 0..half {
21075 let fr = 1.0f32 / 10000f32.powf(2.0 * i as f32 / dh as f32);
21076 cos_d[p * half + i] = (p as f32 * fr).cos();
21077 sin_d[p * half + i] = (p as f32 * fr).sin();
21078 }
21079 }
21080 let base_h: Vec<f32> = (0..s * d).map(|i| ((i % 11) as f32 - 5.0) * 0.1).collect();
21081 let run = |hin: &[f32]| {
21082 run_graph(
21083 &g,
21084 &[
21085 (hidden, hin),
21086 (wqkv, &wqkv_d),
21087 (wo, &wo_d),
21088 (cos, &cos_d),
21089 (sin, &sin_d),
21090 ],
21091 out,
21092 s * d,
21093 )
21094 };
21095 let a = run(&base_h);
21096 let mut changed = base_h.clone();
21098 for j in 0..d {
21099 changed[4 * d + j] += 1.0;
21100 }
21101 let b = run(&changed);
21102 for pos in 0..4 {
21103 for j in 0..d {
21104 let i = pos * d + j;
21105 assert!(
21106 (a[i] - b[i]).abs() < 1e-5,
21107 "causal leak at pos {pos}: {} vs {}",
21108 a[i],
21109 b[i]
21110 );
21111 }
21112 }
21113 let last: f32 = (0..d).map(|j| (a[4 * d + j] - b[4 * d + j]).abs()).sum();
21114 assert!(last > 1e-4, "last position must react to its own token");
21115 }
21116
21117 fn run_graph(
21128 g: &Graph,
21129 inputs: &[(NodeId, &[f32])],
21130 out_id: NodeId,
21131 out_len: usize,
21132 ) -> Vec<f32> {
21133 let plan = rlx_opt::memory::plan_memory(g);
21134 let mut arena = crate::arena::Arena::from_plan(plan);
21135 let sched = compile_thunks(g, &arena);
21136 for &(id, data) in inputs {
21137 let off = arena.byte_offset(id);
21138 let buf = arena.raw_buf_mut();
21139 unsafe {
21140 let p = buf.as_mut_ptr().add(off) as *mut f32;
21141 for (i, &v) in data.iter().enumerate() {
21142 *p.add(i) = v;
21143 }
21144 }
21145 }
21146 execute_thunks(&sched, arena.raw_buf_mut());
21147 let off = arena.byte_offset(out_id);
21148 unsafe {
21149 let p = arena.raw_buf().as_ptr().add(off) as *const f32;
21150 (0..out_len).map(|i| *p.add(i)).collect()
21151 }
21152 }
21153
21154 #[test]
21155 fn relu_backward_matches_mask() {
21156 let f = DType::F32;
21157 let len = 7usize;
21158 let x: Vec<f32> = vec![-2.0, -0.1, 0.0, 0.1, 1.0, 3.0, -5.0];
21159 let dy: Vec<f32> = vec![0.5, 1.5, 2.5, -0.7, 4.0, -1.0, 9.0];
21160
21161 let mut g = Graph::new("relu_bw");
21162 let xn = g.input("x", Shape::new(&[len], f));
21163 let dyn_ = g.input("dy", Shape::new(&[len], f));
21164 let dx = g.relu_backward(xn, dyn_);
21165 g.set_outputs(vec![dx]);
21166
21167 let actual = run_graph(&g, &[(xn, &x), (dyn_, &dy)], dx, len);
21168 let expected: Vec<f32> = x
21172 .iter()
21173 .zip(&dy)
21174 .map(|(&xi, &dyi)| if xi > 0.0 { dyi } else { 0.0 })
21175 .collect();
21176 for (a, e) in actual.iter().zip(&expected) {
21177 assert!((a - e).abs() < 1e-6, "relu_bw mismatch: {a} vs {e}");
21178 }
21179 }
21180
21181 #[test]
21182 fn maxpool2d_backward_routes_to_argmax() {
21183 let f = DType::F32;
21184 let x: Vec<f32> = vec![
21186 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,
21187 ];
21188 let dy: Vec<f32> = vec![0.5, 1.0, 2.0, 4.0];
21192
21193 let mut g = Graph::new("maxpool_bw");
21194 let xn = g.input("x", Shape::new(&[1, 1, 4, 4], f));
21195 let dyn_ = g.input("dy", Shape::new(&[1, 1, 2, 2], f));
21196 let dx = g.maxpool2d_backward(xn, dyn_, vec![2, 2], vec![2, 2], vec![0, 0]);
21197 g.set_outputs(vec![dx]);
21198
21199 let actual = run_graph(&g, &[(xn, &x), (dyn_, &dy)], dx, 16);
21200 let mut expected = vec![0f32; 16];
21201 expected[5] = 0.5;
21202 expected[7] = 1.0;
21203 expected[13] = 2.0;
21204 expected[15] = 4.0;
21205 for (i, (a, e)) in actual.iter().zip(&expected).enumerate() {
21206 assert!((a - e).abs() < 1e-6, "maxpool_bw[{i}] mismatch: {a} vs {e}");
21207 }
21208 }
21209
21210 #[test]
21211 fn conv2d_backward_input_matches_numerical_gradient() {
21212 use rlx_ir::Philox4x32;
21213 let n = 1usize;
21216 let c_in = 2usize;
21217 let h = 4usize;
21218 let w = 4usize;
21219 let c_out = 3usize;
21220 let kh = 3usize;
21221 let kw = 3usize;
21222 let ph = 1usize;
21223 let pw = 1usize;
21224 let sh = 1usize;
21225 let sw = 1usize;
21226 let h_out = (h + 2 * ph - kh) / sh + 1;
21228 let w_out = (w + 2 * pw - kw) / sw + 1;
21229 assert_eq!(h_out, 4);
21230 assert_eq!(w_out, 4);
21231
21232 let mut rng = Philox4x32::new(7);
21233 let mut x = vec![0f32; n * c_in * h * w];
21234 rng.fill_normal(&mut x);
21235 let mut wt = vec![0f32; c_out * c_in * kh * kw];
21236 rng.fill_normal(&mut wt);
21237 let mut dy = vec![0f32; n * c_out * h_out * w_out];
21238 rng.fill_normal(&mut dy);
21239
21240 let f = DType::F32;
21242 let mut g = Graph::new("conv_bwi");
21243 let dy_in = g.input("dy", Shape::new(&[n, c_out, h_out, w_out], f));
21244 let w_in = g.input("w", Shape::new(&[c_out, c_in, kh, kw], f));
21245 let dx = g.conv2d_backward_input(
21246 dy_in,
21247 w_in,
21248 Shape::new(&[n, c_in, h, w], f),
21249 vec![kh, kw],
21250 vec![sh, sw],
21251 vec![ph, pw],
21252 vec![1, 1],
21253 1,
21254 );
21255 g.set_outputs(vec![dx]);
21256 let analytical = run_graph(&g, &[(dy_in, &dy), (w_in, &wt)], dx, n * c_in * h * w);
21257
21258 let forward = |x: &[f32]| -> Vec<f32> {
21262 let mut out = vec![0f32; n * c_out * h_out * w_out];
21263 for ni in 0..n {
21264 for co in 0..c_out {
21265 for ho in 0..h_out {
21266 for wo in 0..w_out {
21267 let mut acc = 0f32;
21268 for ci in 0..c_in {
21269 for ki in 0..kh {
21270 for kj in 0..kw {
21271 let hi = ho * sh + ki;
21272 let wi = wo * sw + kj;
21273 if hi < ph || wi < pw {
21274 continue;
21275 }
21276 let hi = hi - ph;
21277 let wi = wi - pw;
21278 if hi >= h || wi >= w {
21279 continue;
21280 }
21281 let xv = x[((ni * c_in) + ci) * h * w + hi * w + wi];
21282 let wv = wt[((co * c_in) + ci) * kh * kw + ki * kw + kj];
21283 acc += xv * wv;
21284 }
21285 }
21286 }
21287 out[((ni * c_out) + co) * h_out * w_out + ho * w_out + wo] = acc;
21288 }
21289 }
21290 }
21291 }
21292 out
21293 };
21294 let dot = |a: &[f32], b: &[f32]| -> f32 { a.iter().zip(b).map(|(&u, &v)| u * v).sum() };
21295 let eps = 1e-3f32;
21296 let mut numerical = vec![0f32; x.len()];
21297 for i in 0..x.len() {
21298 let saved = x[i];
21299 x[i] = saved + eps;
21300 let plus = dot(&forward(&x), &dy);
21301 x[i] = saved - eps;
21302 let minus = dot(&forward(&x), &dy);
21303 x[i] = saved;
21304 numerical[i] = (plus - minus) / (2.0 * eps);
21305 }
21306 for (i, (a, n)) in analytical.iter().zip(&numerical).enumerate() {
21307 assert!(
21309 (a - n).abs() < 5e-3,
21310 "conv_bw_input[{i}]: analytical {a} vs numerical {n}"
21311 );
21312 }
21313 }
21314
21315 #[test]
21316 fn conv2d_backward_weight_matches_numerical_gradient() {
21317 use rlx_ir::Philox4x32;
21318 let n = 2usize;
21319 let c_in = 2usize;
21320 let h = 4usize;
21321 let w = 4usize;
21322 let c_out = 2usize;
21323 let kh = 3usize;
21324 let kw = 3usize;
21325 let ph = 0usize;
21326 let pw = 0usize;
21327 let sh = 1usize;
21328 let sw = 1usize;
21329 let h_out = (h + 2 * ph - kh) / sh + 1;
21330 let w_out = (w + 2 * pw - kw) / sw + 1;
21331
21332 let mut rng = Philox4x32::new(11);
21333 let mut x = vec![0f32; n * c_in * h * w];
21334 rng.fill_normal(&mut x);
21335 let mut wt = vec![0f32; c_out * c_in * kh * kw];
21336 rng.fill_normal(&mut wt);
21337 let mut dy = vec![0f32; n * c_out * h_out * w_out];
21338 rng.fill_normal(&mut dy);
21339
21340 let f = DType::F32;
21341 let mut g = Graph::new("conv_bww");
21342 let xn = g.input("x", Shape::new(&[n, c_in, h, w], f));
21343 let dyn_ = g.input("dy", Shape::new(&[n, c_out, h_out, w_out], f));
21344 let dwn = g.conv2d_backward_weight(
21345 xn,
21346 dyn_,
21347 Shape::new(&[c_out, c_in, kh, kw], f),
21348 vec![kh, kw],
21349 vec![sh, sw],
21350 vec![ph, pw],
21351 vec![1, 1],
21352 1,
21353 );
21354 g.set_outputs(vec![dwn]);
21355 let analytical = run_graph(&g, &[(xn, &x), (dyn_, &dy)], dwn, c_out * c_in * kh * kw);
21356
21357 let forward = |wt: &[f32]| -> Vec<f32> {
21358 let mut out = vec![0f32; n * c_out * h_out * w_out];
21359 for ni in 0..n {
21360 for co in 0..c_out {
21361 for ho in 0..h_out {
21362 for wo in 0..w_out {
21363 let mut acc = 0f32;
21364 for ci in 0..c_in {
21365 for ki in 0..kh {
21366 for kj in 0..kw {
21367 let hi = ho + ki;
21368 let wi = wo + kj;
21369 let xv = x[((ni * c_in) + ci) * h * w + hi * w + wi];
21370 let wv = wt[((co * c_in) + ci) * kh * kw + ki * kw + kj];
21371 acc += xv * wv;
21372 }
21373 }
21374 }
21375 out[((ni * c_out) + co) * h_out * w_out + ho * w_out + wo] = acc;
21376 }
21377 }
21378 }
21379 }
21380 out
21381 };
21382 let dot = |a: &[f32], b: &[f32]| -> f32 { a.iter().zip(b).map(|(&u, &v)| u * v).sum() };
21383 let eps = 1e-3f32;
21384 let mut numerical = vec![0f32; wt.len()];
21385 for i in 0..wt.len() {
21386 let saved = wt[i];
21387 wt[i] = saved + eps;
21388 let plus = dot(&forward(&wt), &dy);
21389 wt[i] = saved - eps;
21390 let minus = dot(&forward(&wt), &dy);
21391 wt[i] = saved;
21392 numerical[i] = (plus - minus) / (2.0 * eps);
21393 }
21394 for (i, (a, n)) in analytical.iter().zip(&numerical).enumerate() {
21395 assert!(
21396 (a - n).abs() < 5e-3,
21397 "conv_bw_weight[{i}]: analytical {a} vs numerical {n}"
21398 );
21399 }
21400 }
21401
21402 #[test]
21403 fn softmax_cross_entropy_matches_reference() {
21404 let f = DType::F32;
21405 let logits: Vec<f32> = vec![
21406 1.0, 2.0, 3.0, -1.0, 0.0, 4.0, 5.0, 5.0, 5.0, ];
21410 let labels: Vec<f32> = vec![2.0, 0.0, 1.0];
21411
21412 let mut g = Graph::new("sce");
21413 let lg = g.input("logits", Shape::new(&[3, 3], f));
21414 let lb = g.input("labels", Shape::new(&[3], f));
21415 let loss = g.softmax_cross_entropy_with_logits(lg, lb);
21416 g.set_outputs(vec![loss]);
21417 let actual = run_graph(&g, &[(lg, &logits), (lb, &labels)], loss, 3);
21418
21419 let mut expected = vec![0f32; 3];
21421 for ni in 0..3 {
21422 let row = &logits[ni * 3..(ni + 1) * 3];
21423 let m = row.iter().fold(f32::NEG_INFINITY, |a, &v| a.max(v));
21424 let sum: f32 = row.iter().map(|&v| (v - m).exp()).sum();
21425 let lse = m + sum.ln();
21426 let label_idx = labels[ni] as usize;
21427 expected[ni] = lse - row[label_idx];
21428 }
21429 for (i, (a, e)) in actual.iter().zip(&expected).enumerate() {
21430 assert!((a - e).abs() < 1e-5, "sce loss[{i}]: {a} vs {e}");
21431 }
21432 }
21433
21434 #[test]
21435 fn softmax_cross_entropy_backward_matches_numerical_gradient() {
21436 use rlx_ir::Philox4x32;
21437 let n = 4usize;
21438 let c = 5usize;
21439 let mut rng = Philox4x32::new(23);
21440 let mut logits = vec![0f32; n * c];
21441 rng.fill_normal(&mut logits);
21442 let labels: Vec<f32> = (0..n).map(|i| (i % c) as f32).collect();
21443 let mut d_loss = vec![0f32; n];
21444 rng.fill_normal(&mut d_loss);
21445
21446 let f = DType::F32;
21447 let mut g = Graph::new("sce_bw");
21448 let lg = g.input("logits", Shape::new(&[n, c], f));
21449 let lb = g.input("labels", Shape::new(&[n], f));
21450 let dl = g.input("d_loss", Shape::new(&[n], f));
21451 let dlogits = g.softmax_cross_entropy_backward(lg, lb, dl);
21452 g.set_outputs(vec![dlogits]);
21453 let analytical = run_graph(
21454 &g,
21455 &[(lg, &logits), (lb, &labels), (dl, &d_loss)],
21456 dlogits,
21457 n * c,
21458 );
21459
21460 let sce_loss = |logits: &[f32]| -> Vec<f32> {
21462 let mut out = vec![0f32; n];
21463 for ni in 0..n {
21464 let row = &logits[ni * c..(ni + 1) * c];
21465 let m = row.iter().fold(f32::NEG_INFINITY, |a, &v| a.max(v));
21466 let sum: f32 = row.iter().map(|&v| (v - m).exp()).sum();
21467 out[ni] = (m + sum.ln()) - row[labels[ni] as usize];
21468 }
21469 out
21470 };
21471 let dot = |a: &[f32], b: &[f32]| a.iter().zip(b).map(|(&u, &v)| u * v).sum::<f32>();
21472 let eps = 1e-3f32;
21473 let mut numerical = vec![0f32; logits.len()];
21474 for i in 0..logits.len() {
21475 let saved = logits[i];
21476 logits[i] = saved + eps;
21477 let plus = dot(&sce_loss(&logits), &d_loss);
21478 logits[i] = saved - eps;
21479 let minus = dot(&sce_loss(&logits), &d_loss);
21480 logits[i] = saved;
21481 numerical[i] = (plus - minus) / (2.0 * eps);
21482 }
21483 for (i, (a, num)) in analytical.iter().zip(&numerical).enumerate() {
21484 assert!(
21485 (a - num).abs() < 5e-3,
21486 "sce_bw[{i}]: analytical {a} vs numerical {num}"
21487 );
21488 }
21489 }
21490
21491 fn fill_constants_into_arena(graph: &Graph, arena: &mut crate::arena::Arena) {
21504 for node in graph.nodes() {
21505 if let Op::Constant { data } = &node.op
21506 && arena.has_buffer(node.id)
21507 && !data.is_empty()
21508 {
21509 let buf = arena.slice_mut(node.id);
21510 let n_floats = data.len() / 4;
21511 let n = buf.len().min(n_floats);
21512 for i in 0..n {
21513 let bytes = [
21514 data[i * 4],
21515 data[i * 4 + 1],
21516 data[i * 4 + 2],
21517 data[i * 4 + 3],
21518 ];
21519 buf[i] = f32::from_le_bytes(bytes);
21520 }
21521 }
21522 }
21523 }
21524
21525 fn prepare(
21529 graph: &Graph,
21530 seed_inputs: &[(NodeId, &[f32])],
21531 ) -> (ThunkSchedule, crate::arena::Arena) {
21532 let plan = rlx_opt::memory::plan_memory(graph);
21533 let mut arena = crate::arena::Arena::from_plan(plan);
21534 let sched = compile_thunks(graph, &arena);
21535 fill_constants_into_arena(graph, &mut arena);
21536 for &(id, data) in seed_inputs {
21537 let off = arena.byte_offset(id);
21538 let buf = arena.raw_buf_mut();
21539 unsafe {
21540 let p = buf.as_mut_ptr().add(off) as *mut f32;
21541 for (i, &v) in data.iter().enumerate() {
21542 *p.add(i) = v;
21543 }
21544 }
21545 }
21546 (sched, arena)
21547 }
21548
21549 fn read_arena(arena: &crate::arena::Arena, id: NodeId, len: usize) -> Vec<f32> {
21550 let off = arena.byte_offset(id);
21551 unsafe {
21552 let p = arena.raw_buf().as_ptr().add(off) as *const f32;
21553 (0..len).map(|i| *p.add(i)).collect()
21554 }
21555 }
21556
21557 fn write_arena(arena: &mut crate::arena::Arena, id: NodeId, data: &[f32]) {
21558 let off = arena.byte_offset(id);
21559 let buf = arena.raw_buf_mut();
21560 unsafe {
21561 let p = buf.as_mut_ptr().add(off) as *mut f32;
21562 for (i, &v) in data.iter().enumerate() {
21563 *p.add(i) = v;
21564 }
21565 }
21566 }
21567
21568 fn prepare_f64(
21570 graph: &Graph,
21571 seed_inputs: &[(NodeId, &[f64])],
21572 ) -> (ThunkSchedule, crate::arena::Arena) {
21573 let plan = rlx_opt::memory::plan_memory(graph);
21574 let mut arena = crate::arena::Arena::from_plan(plan);
21575 let sched = compile_thunks(graph, &arena);
21576 fill_constants_into_arena(graph, &mut arena);
21577 for &(id, data) in seed_inputs {
21578 let off = arena.byte_offset(id);
21579 let buf = arena.raw_buf_mut();
21580 unsafe {
21581 let p = buf.as_mut_ptr().add(off) as *mut f64;
21582 for (i, &v) in data.iter().enumerate() {
21583 *p.add(i) = v;
21584 }
21585 }
21586 }
21587 (sched, arena)
21588 }
21589
21590 fn read_arena_f64(arena: &crate::arena::Arena, id: NodeId, len: usize) -> Vec<f64> {
21591 let off = arena.byte_offset(id);
21592 unsafe {
21593 let p = arena.raw_buf().as_ptr().add(off) as *const f64;
21594 (0..len).map(|i| *p.add(i)).collect()
21595 }
21596 }
21597
21598 #[test]
21608 fn dense_solve_f64_end_to_end() {
21609 let mut g = Graph::new("solve_e2e");
21610 let a = g.input("A", Shape::new(&[2, 2], DType::F64));
21611 let b = g.input("b", Shape::new(&[2], DType::F64));
21612 let x = g.dense_solve(a, b, Shape::new(&[2], DType::F64));
21613 g.set_outputs(vec![x]);
21614
21615 let a_data = [2.0, 1.0, 1.0, 3.0_f64];
21616 let b_data = [5.0, 10.0_f64];
21617 let (sched, mut arena) = prepare_f64(&g, &[(a, &a_data), (b, &b_data)]);
21618 execute_thunks(&sched, arena.raw_buf_mut());
21619
21620 let got = read_arena_f64(&arena, x, 2);
21621 let want = [1.0, 3.0_f64];
21622 for i in 0..2 {
21623 assert!(
21624 (got[i] - want[i]).abs() < 1e-12,
21625 "x[{i}] = {} (expected {})",
21626 got[i],
21627 want[i]
21628 );
21629 }
21630 }
21631
21632 #[test]
21638 fn dense_solve_f64_5x5_laplacian() {
21639 let n = 5usize;
21640 let mut g = Graph::new("solve_5x5");
21641 let a = g.input("A", Shape::new(&[n, n], DType::F64));
21642 let b = g.input("b", Shape::new(&[n], DType::F64));
21643 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
21644 g.set_outputs(vec![x]);
21645
21646 let mut a_data = vec![0.0_f64; n * n];
21648 for i in 0..n {
21649 a_data[i * n + i] = 2.0;
21650 if i > 0 {
21651 a_data[i * n + (i - 1)] = -1.0;
21652 }
21653 if i + 1 < n {
21654 a_data[i * n + (i + 1)] = -1.0;
21655 }
21656 }
21657 let b_data: Vec<f64> = (0..n).map(|i| (i + 1) as f64).collect();
21658 let (sched, mut arena) = prepare_f64(&g, &[(a, &a_data), (b, &b_data)]);
21659 execute_thunks(&sched, arena.raw_buf_mut());
21660
21661 let got = read_arena_f64(&arena, x, n);
21662 let mut residual = vec![0.0_f64; n];
21664 for i in 0..n {
21665 for j in 0..n {
21666 residual[i] += a_data[i * n + j] * got[j];
21667 }
21668 }
21669 for i in 0..n {
21670 assert!(
21671 (residual[i] - b_data[i]).abs() < 1e-10,
21672 "row {i}: residual {} vs b {}",
21673 residual[i],
21674 b_data[i]
21675 );
21676 }
21677 }
21678
21679 #[test]
21698 fn hello_resistor_gradient_end_to_end() {
21699 use rlx_opt::autodiff::grad_with_loss;
21700 let n = 3usize;
21701
21702 let mut g = Graph::new("hello_resistor");
21704 let a = g.param("A", Shape::new(&[n, n], DType::F64));
21705 let b = g.input("b", Shape::new(&[n], DType::F64));
21706 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
21707 let loss = g.reduce(
21708 x,
21709 ReduceOp::Sum,
21710 vec![0],
21711 false,
21712 Shape::new(&[1], DType::F64),
21713 );
21714 g.set_outputs(vec![loss]);
21715
21716 let bwd = grad_with_loss(&g, &[a, b]);
21718 assert_eq!(bwd.outputs.len(), 3, "expect [loss, dA, db]");
21719
21720 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
21724 for node in graph.nodes() {
21725 let name = match &node.op {
21726 rlx_ir::Op::Input { name } => Some(name.as_str()),
21727 rlx_ir::Op::Param { name } => Some(name.as_str()),
21728 _ => None,
21729 };
21730 if name == Some(want) {
21731 return node.id;
21732 }
21733 }
21734 panic!("no node named {want:?} in bwd graph");
21735 };
21736 let a_bwd = find_by_name(&bwd, "A");
21737 let b_bwd = find_by_name(&bwd, "b");
21738 let d_out_bwd = find_by_name(&bwd, "d_output");
21739
21740 let a_data = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0_f64];
21744 let b_data = [1.0, 2.0, 3.0_f64];
21745 let d_output = [1.0_f64]; let (sched, mut arena) = prepare_f64(
21749 &bwd,
21750 &[(a_bwd, &a_data), (b_bwd, &b_data), (d_out_bwd, &d_output)],
21751 );
21752 execute_thunks(&sched, arena.raw_buf_mut());
21753
21754 let loss_out = read_arena_f64(&arena, bwd.outputs[0], 1);
21755 let da_out = read_arena_f64(&arena, bwd.outputs[1], n * n);
21756 let db_out = read_arena_f64(&arena, bwd.outputs[2], n);
21757
21758 let x_ref = {
21761 let mut a = a_data;
21762 let mut b = b_data;
21763 let info = crate::blas::dgesv(&mut a, &mut b, n, 1);
21764 assert_eq!(info, 0);
21765 b
21766 };
21767 let loss_ref: f64 = x_ref.iter().sum();
21768 let db_ref = {
21770 let mut at = [0.0_f64; 9];
21771 for i in 0..n {
21772 for j in 0..n {
21773 at[i * n + j] = a_data[j * n + i];
21774 }
21775 }
21776 let mut ones = [1.0_f64; 3];
21777 let info = crate::blas::dgesv(&mut at, &mut ones, n, 1);
21778 assert_eq!(info, 0);
21779 ones
21780 };
21781 let mut da_ref = [0.0_f64; 9];
21783 for i in 0..n {
21784 for j in 0..n {
21785 da_ref[i * n + j] = -db_ref[i] * x_ref[j];
21786 }
21787 }
21788
21789 assert!(
21791 (loss_out[0] - loss_ref).abs() < 1e-10,
21792 "loss: got {}, want {}",
21793 loss_out[0],
21794 loss_ref
21795 );
21796 for i in 0..n {
21797 assert!(
21798 (db_out[i] - db_ref[i]).abs() < 1e-10,
21799 "db[{i}]: got {}, want {}",
21800 db_out[i],
21801 db_ref[i]
21802 );
21803 }
21804 for i in 0..n * n {
21805 assert!(
21806 (da_out[i] - da_ref[i]).abs() < 1e-10,
21807 "dA[{i}]: got {}, want {}",
21808 da_out[i],
21809 da_ref[i]
21810 );
21811 }
21812
21813 let h = 1e-6_f64;
21816 for k in 0..n {
21817 let mut bp = b_data;
21818 bp[k] += h;
21819 let mut bm = b_data;
21820 bm[k] -= h;
21821 let lp = {
21822 let mut ac = a_data;
21823 let info = crate::blas::dgesv(&mut ac, &mut bp, n, 1);
21824 assert_eq!(info, 0);
21825 bp.iter().sum::<f64>()
21826 };
21827 let lm = {
21828 let mut ac = a_data;
21829 let info = crate::blas::dgesv(&mut ac, &mut bm, n, 1);
21830 assert_eq!(info, 0);
21831 bm.iter().sum::<f64>()
21832 };
21833 let fd = (lp - lm) / (2.0 * h);
21834 assert!(
21835 (db_out[k] - fd).abs() < 1e-7,
21836 "FD mismatch on db[{k}]: AD={} FD={}",
21837 db_out[k],
21838 fd
21839 );
21840 }
21841 }
21842
21843 #[test]
21848 fn scan_geometric_growth_f64() {
21849 let n = 3usize;
21850 let length = 10u32;
21851
21852 let mut body = Graph::new("scan_body");
21854 let x = body.input("carry", Shape::new(&[n], DType::F64));
21855 let scale_bytes: Vec<u8> = (0..n).flat_map(|_| 0.1_f64.to_le_bytes()).collect();
21856 let scale = body.add_node(
21857 Op::Constant { data: scale_bytes },
21858 vec![],
21859 Shape::new(&[n], DType::F64),
21860 );
21861 let scaled = body.binary(BinaryOp::Mul, x, scale, Shape::new(&[n], DType::F64));
21862 let next = body.binary(BinaryOp::Add, x, scaled, Shape::new(&[n], DType::F64));
21863 body.set_outputs(vec![next]);
21864
21865 let mut g = Graph::new("scan_outer");
21867 let init = g.input("init", Shape::new(&[n], DType::F64));
21868 let final_carry = g.scan(init, body, length);
21869 g.set_outputs(vec![final_carry]);
21870
21871 let init_data = vec![1.0_f64; n];
21872 let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data)]);
21873 execute_thunks(&sched, arena.raw_buf_mut());
21874 let got = read_arena_f64(&arena, final_carry, n);
21875 let want: f64 = 1.1_f64.powi(length as i32);
21876 for i in 0..n {
21877 assert!(
21878 (got[i] - want).abs() < 1e-12,
21879 "got[{i}] = {} want {}",
21880 got[i],
21881 want
21882 );
21883 }
21884 }
21885
21886 #[test]
21893 fn scan_with_xs_cumulative_sum() {
21894 let n = 3usize;
21895 let length = 4u32;
21896
21897 let mut body = Graph::new("cumsum_body");
21898 let carry = body.input("carry", Shape::new(&[n], DType::F64));
21900 let x_t = body.input("x_t", Shape::new(&[n], DType::F64));
21901 let next = body.binary(BinaryOp::Add, carry, x_t, Shape::new(&[n], DType::F64));
21902 body.set_outputs(vec![next]);
21903
21904 let mut g = Graph::new("cumsum_outer");
21905 let init = g.input("init", Shape::new(&[n], DType::F64));
21906 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
21907 let final_carry = g.scan_with_xs(init, &[xs], body, length);
21908 g.set_outputs(vec![final_carry]);
21909
21910 let init_data = vec![0.0_f64; n];
21911 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)]);
21913 execute_thunks(&sched, arena.raw_buf_mut());
21914 let got = read_arena_f64(&arena, final_carry, n);
21915
21916 let mut want = init_data.clone();
21920 for t in 0..length as usize {
21921 for j in 0..n {
21922 want[j] += xs_data[t * n + j];
21923 }
21924 }
21925 for i in 0..n {
21926 assert!(
21927 (got[i] - want[i]).abs() < 1e-12,
21928 "got[{i}] = {} want {}",
21929 got[i],
21930 want[i]
21931 );
21932 }
21933 }
21934
21935 #[test]
21939 fn scan_with_xs_be_with_drive() {
21940 let n = 3usize;
21941 let length = 4u32;
21942 let dt = 0.1_f64;
21943
21944 let mut m_data = vec![0.0_f64; n * n];
21945 for i in 0..n {
21946 m_data[i * n + i] = 1.0 + dt * 2.0;
21947 if i > 0 {
21948 m_data[i * n + (i - 1)] = -dt;
21949 }
21950 if i + 1 < n {
21951 m_data[i * n + (i + 1)] = -dt;
21952 }
21953 }
21954 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
21955
21956 let mut body = Graph::new("be_drive_body");
21957 let carry = body.input("carry", Shape::new(&[n], DType::F64));
21958 let drive = body.input("drive", Shape::new(&[n], DType::F64));
21959 let m = body.add_node(
21960 Op::Constant { data: m_bytes },
21961 vec![],
21962 Shape::new(&[n, n], DType::F64),
21963 );
21964 let driven = body.binary(BinaryOp::Add, carry, drive, Shape::new(&[n], DType::F64));
21965 let next = body.dense_solve(m, driven, Shape::new(&[n], DType::F64));
21966 body.set_outputs(vec![next]);
21967
21968 let mut g = Graph::new("be_drive_outer");
21969 let init = g.input("init", Shape::new(&[n], DType::F64));
21970 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
21971 let final_carry = g.scan_with_xs(init, &[xs], body, length);
21972 g.set_outputs(vec![final_carry]);
21973
21974 let init_data = vec![0.0_f64; n];
21975 let mut xs_data = vec![0.0_f64; length as usize * n];
21978 xs_data[0] = 1.0;
21979
21980 let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data), (xs, &xs_data)]);
21981 execute_thunks(&sched, arena.raw_buf_mut());
21982 let got = read_arena_f64(&arena, final_carry, n);
21983
21984 let mut x = init_data.clone();
21986 for t in 0..length as usize {
21987 for j in 0..n {
21988 x[j] += xs_data[t * n + j];
21989 }
21990 let mut a_copy = m_data.clone();
21991 crate::blas::dgesv(&mut a_copy, &mut x, n, 1);
21992 }
21993 for i in 0..n {
21994 assert!(
21995 (got[i] - x[i]).abs() < 1e-12,
21996 "got[{i}] = {} ref {}",
21997 got[i],
21998 x[i]
21999 );
22000 }
22001 }
22002
22003 #[test]
22009 fn batched_dense_solve_gradient_matches_per_batch_analytic() {
22010 use rlx_opt::autodiff::grad_with_loss;
22011 let n = 3usize;
22012 let batch = 4usize;
22013
22014 let mut g = Graph::new("bds_grad");
22015 let a = g.param("A", Shape::new(&[batch, n, n], DType::F64));
22016 let b = g.input("b", Shape::new(&[batch, n], DType::F64));
22017 let x = g.batched_dense_solve(a, b, Shape::new(&[batch, n], DType::F64));
22018 let loss = g.reduce(
22019 x,
22020 ReduceOp::Sum,
22021 vec![0, 1],
22022 false,
22023 Shape::new(&[1], DType::F64),
22024 );
22025 g.set_outputs(vec![loss]);
22026
22027 let bwd = grad_with_loss(&g, &[a, b]);
22028
22029 let find = |graph: &Graph, want: &str| -> NodeId {
22030 for node in graph.nodes() {
22031 let name = match &node.op {
22032 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
22033 _ => None,
22034 };
22035 if name == Some(want) {
22036 return node.id;
22037 }
22038 }
22039 panic!("no node named {want}");
22040 };
22041 let a_id = find(&bwd, "A");
22042 let b_id = find(&bwd, "b");
22043 let d_out_id = find(&bwd, "d_output");
22044
22045 let mut rng = rlx_ir::Philox4x32::new(0x57e1_u64);
22046 let mut a_data = vec![0.0_f64; batch * n * n];
22047 let mut b_data = vec![0.0_f64; batch * n];
22048 for bi in 0..batch {
22049 for i in 0..n {
22050 for j in 0..n {
22051 a_data[bi * n * n + i * n + j] = rng.next_f32() as f64 * 0.1;
22052 }
22053 a_data[bi * n * n + i * n + i] += 1.0 + n as f64;
22054 }
22055 for i in 0..n {
22056 b_data[bi * n + i] = rng.next_f32() as f64;
22057 }
22058 }
22059 let d_seed = [1.0_f64];
22060
22061 let (sched, mut arena) = prepare_f64(
22062 &bwd,
22063 &[(a_id, &a_data), (b_id, &b_data), (d_out_id, &d_seed)],
22064 );
22065 execute_thunks(&sched, arena.raw_buf_mut());
22066 let da_out = read_arena_f64(&arena, bwd.outputs[1], batch * n * n);
22067 let db_out = read_arena_f64(&arena, bwd.outputs[2], batch * n);
22068
22069 for bi in 0..batch {
22072 let a_slice: Vec<f64> = a_data[bi * n * n..(bi + 1) * n * n].to_vec();
22073 let mut b_slice: Vec<f64> = b_data[bi * n..(bi + 1) * n].to_vec();
22074 let mut a_copy = a_slice.clone();
22075 crate::blas::dgesv(&mut a_copy, &mut b_slice, n, 1);
22076 let x_ref = b_slice.clone();
22077 let mut at = vec![0.0_f64; n * n];
22079 for i in 0..n {
22080 for j in 0..n {
22081 at[i * n + j] = a_slice[j * n + i];
22082 }
22083 }
22084 let mut ones = vec![1.0_f64; n];
22085 crate::blas::dgesv(&mut at, &mut ones, n, 1);
22086 let db_ref = ones;
22087 for i in 0..n {
22088 let got = db_out[bi * n + i];
22089 assert!(
22090 (got - db_ref[i]).abs() < 1e-10,
22091 "batch {bi}, db[{i}]: got {got} ref {}",
22092 db_ref[i]
22093 );
22094 }
22095 for i in 0..n {
22097 for j in 0..n {
22098 let got = da_out[bi * n * n + i * n + j];
22099 let want = -db_ref[i] * x_ref[j];
22100 assert!(
22101 (got - want).abs() < 1e-10,
22102 "batch {bi}, dA[{i},{j}]: got {got} ref {want}"
22103 );
22104 }
22105 }
22106 }
22107 }
22108
22109 #[test]
22114 fn scan_checkpointed_grad_matches_plain_scan_grad() {
22115 use rlx_opt::autodiff::grad_with_loss;
22116 let n = 2usize;
22117 let length = 6u32;
22118
22119 let make_body = || {
22120 let mut body = Graph::new("ck_body");
22121 let carry = body.input("carry", Shape::new(&[n], DType::F64));
22122 let scale_bytes: Vec<u8> = (0..n).flat_map(|_| 1.05_f64.to_le_bytes()).collect();
22123 let scale = body.add_node(
22124 Op::Constant { data: scale_bytes },
22125 vec![],
22126 Shape::new(&[n], DType::F64),
22127 );
22128 let next = body.binary(BinaryOp::Mul, carry, scale, Shape::new(&[n], DType::F64));
22129 body.set_outputs(vec![next]);
22130 body
22131 };
22132
22133 let mut g_plain = Graph::new("ck_plain");
22135 let init_p = g_plain.input("init", Shape::new(&[n], DType::F64));
22136 let final_p = g_plain.scan(init_p, make_body(), length);
22137 let loss_p = g_plain.reduce(
22138 final_p,
22139 ReduceOp::Sum,
22140 vec![0],
22141 false,
22142 Shape::new(&[1], DType::F64),
22143 );
22144 g_plain.set_outputs(vec![loss_p]);
22145 let bwd_p = grad_with_loss(&g_plain, &[init_p]);
22146
22147 let mut g_ck = Graph::new("ck_ckpt");
22149 let init_c = g_ck.input("init", Shape::new(&[n], DType::F64));
22150 let final_c = g_ck.scan_checkpointed(init_c, make_body(), length, 2);
22151 let loss_c = g_ck.reduce(
22152 final_c,
22153 ReduceOp::Sum,
22154 vec![0],
22155 false,
22156 Shape::new(&[1], DType::F64),
22157 );
22158 g_ck.set_outputs(vec![loss_c]);
22159 let bwd_c = grad_with_loss(&g_ck, &[init_c]);
22160
22161 let find = |graph: &Graph, want: &str| -> NodeId {
22162 for node in graph.nodes() {
22163 let name = match &node.op {
22164 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
22165 _ => None,
22166 };
22167 if name == Some(want) {
22168 return node.id;
22169 }
22170 }
22171 panic!("no {want}");
22172 };
22173
22174 let init_data = vec![0.5_f64, -0.5];
22175 let d_seed = [1.0_f64];
22176
22177 let (s_p, mut a_p) = prepare_f64(
22178 &bwd_p,
22179 &[
22180 (find(&bwd_p, "init"), &init_data),
22181 (find(&bwd_p, "d_output"), &d_seed),
22182 ],
22183 );
22184 execute_thunks(&s_p, a_p.raw_buf_mut());
22185 let dinit_p = read_arena_f64(&a_p, bwd_p.outputs[1], n);
22186
22187 let (s_c, mut a_c) = prepare_f64(
22188 &bwd_c,
22189 &[
22190 (find(&bwd_c, "init"), &init_data),
22191 (find(&bwd_c, "d_output"), &d_seed),
22192 ],
22193 );
22194 execute_thunks(&s_c, a_c.raw_buf_mut());
22195 let dinit_c = read_arena_f64(&a_c, bwd_c.outputs[1], n);
22196
22197 for i in 0..n {
22198 assert!(
22199 (dinit_p[i] - dinit_c[i]).abs() < 1e-12,
22200 "dinit[{i}]: plain={} checkpointed={}",
22201 dinit_p[i],
22202 dinit_c[i]
22203 );
22204 }
22205 }
22206
22207 #[test]
22213 fn recursive_checkpointing_matches_full_trajectory() {
22214 let n = 2usize;
22215 let length = 4u32;
22216
22217 let build_body = || -> Graph {
22219 let mut body = Graph::new("rc_body");
22220 let carry = body.input("carry", Shape::new(&[n], DType::F64));
22221 let ones_bytes: Vec<u8> = (0..n).flat_map(|_| 1.0_f64.to_le_bytes()).collect();
22222 let ones = body.add_node(
22223 Op::Constant { data: ones_bytes },
22224 vec![],
22225 Shape::new(&[n], DType::F64),
22226 );
22227 let next = body.binary(BinaryOp::Add, carry, ones, Shape::new(&[n], DType::F64));
22228 body.set_outputs(vec![next]);
22229 body
22230 };
22231
22232 let body_vjp_for = || -> Graph {
22235 use rlx_opt::autodiff::grad;
22236 let body = build_body();
22237 let carry_id = body
22239 .nodes()
22240 .iter()
22241 .find(|n| matches!(n.op, Op::Input { .. }))
22242 .map(|n| n.id)
22243 .unwrap();
22244 grad(&body, &[carry_id])
22245 };
22246
22247 let mut g_full = Graph::new("rc_outer_full");
22249 let init_full = g_full.input("init", Shape::new(&[n], DType::F64));
22250 let traj_full_id = g_full.scan_trajectory(init_full, build_body(), length);
22251 let upstream_full = g_full.input("upstream", Shape::new(&[length as usize, n], DType::F64));
22253 let dinit_full_id = g_full.scan_backward(
22254 init_full,
22255 traj_full_id,
22256 upstream_full,
22257 &[],
22258 body_vjp_for(),
22259 length,
22260 true,
22261 Shape::new(&[n], DType::F64),
22262 );
22263 g_full.set_outputs(vec![dinit_full_id]);
22264
22265 let k = 2u32;
22268 let mut g_rec = Graph::new("rc_outer_rec");
22269 let init_rec = g_rec.input("init", Shape::new(&[n], DType::F64));
22270 let traj_rec_id = g_rec.add_node(
22271 Op::Scan {
22272 body: Box::new(build_body()),
22273 length,
22274 save_trajectory: true,
22275 num_bcast: 0,
22276 num_xs: 0,
22277 num_checkpoints: k,
22278 },
22279 vec![init_rec],
22280 Shape::new(&[k as usize, n], DType::F64),
22281 );
22282 let upstream_rec = g_rec.input("upstream", Shape::new(&[length as usize, n], DType::F64));
22285 let dinit_rec_id = g_rec.add_node(
22286 Op::ScanBackward {
22287 body_vjp: Box::new(body_vjp_for()),
22288 length,
22289 save_trajectory: true,
22290 num_xs: 0,
22291 num_checkpoints: k,
22292 forward_body: Some(Box::new(build_body())),
22293 },
22294 vec![init_rec, traj_rec_id, upstream_rec],
22295 Shape::new(&[n], DType::F64),
22296 );
22297 g_rec.set_outputs(vec![dinit_rec_id]);
22298
22299 let init_data = vec![0.5_f64, -0.5];
22301 let upstream_data: Vec<f64> = (0..length as usize * n).map(|i| (i as f64) * 0.1).collect();
22302
22303 let find = |graph: &Graph, want: &str| -> NodeId {
22304 for node in graph.nodes() {
22305 if let Op::Input { name } = &node.op
22306 && name == want
22307 {
22308 return node.id;
22309 }
22310 }
22311 panic!("no input {want}");
22312 };
22313
22314 let (s_full, mut a_full) = prepare_f64(
22315 &g_full,
22316 &[
22317 (find(&g_full, "init"), &init_data),
22318 (find(&g_full, "upstream"), &upstream_data),
22319 ],
22320 );
22321 execute_thunks(&s_full, a_full.raw_buf_mut());
22322 let dinit_full = read_arena_f64(&a_full, g_full.outputs[0], n);
22323
22324 let (s_rec, mut a_rec) = prepare_f64(
22325 &g_rec,
22326 &[
22327 (find(&g_rec, "init"), &init_data),
22328 (find(&g_rec, "upstream"), &upstream_data),
22329 ],
22330 );
22331 execute_thunks(&s_rec, a_rec.raw_buf_mut());
22332 let dinit_rec = read_arena_f64(&a_rec, g_rec.outputs[0], n);
22333
22334 for i in 0..n {
22335 assert!(
22336 (dinit_full[i] - dinit_rec[i]).abs() < 1e-12,
22337 "i={i}: full={} rec={}",
22338 dinit_full[i],
22339 dinit_rec[i]
22340 );
22341 }
22342 }
22343
22344 #[test]
22353 fn vmap_of_grad_scan_matches_per_row_runs() {
22354 use rlx_opt::autodiff::grad_with_loss;
22355 use rlx_opt::vmap::vmap;
22356 let n = 2usize;
22357 let length = 3u32;
22358 let batch = 3usize;
22359
22360 let mut body = Graph::new("scan_grad_body");
22361 let carry = body.input("carry", Shape::new(&[n], DType::F64));
22362 let ones_bytes: Vec<u8> = (0..n).flat_map(|_| 1.0_f64.to_le_bytes()).collect();
22363 let ones = body.add_node(
22364 Op::Constant { data: ones_bytes },
22365 vec![],
22366 Shape::new(&[n], DType::F64),
22367 );
22368 let next = body.binary(BinaryOp::Add, carry, ones, Shape::new(&[n], DType::F64));
22369 body.set_outputs(vec![next]);
22370
22371 let mut g = Graph::new("scan_grad_outer");
22372 let init = g.input("init", Shape::new(&[n], DType::F64));
22373 let final_x = g.scan(init, body, length);
22374 let loss = g.reduce(
22375 final_x,
22376 ReduceOp::Sum,
22377 vec![0],
22378 false,
22379 Shape::new(&[1], DType::F64),
22380 );
22381 g.set_outputs(vec![loss]);
22382
22383 let bwd = grad_with_loss(&g, &[init]);
22384 let bg = vmap(&bwd, &["init"], batch);
22385
22386 let find = |graph: &Graph, want: &str| -> NodeId {
22387 for node in graph.nodes() {
22388 let name = match &node.op {
22389 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
22390 _ => None,
22391 };
22392 if name == Some(want) {
22393 return node.id;
22394 }
22395 }
22396 panic!("no node named {want}");
22397 };
22398 let init_b = find(&bg, "init");
22399 let d_out_b = find(&bg, "d_output");
22400
22401 let init_data: Vec<f64> = (0..batch * n).map(|i| (i as f64) * 0.5).collect();
22402 let d_seed = [1.0_f64];
22403
22404 let (sched, mut arena) = prepare_f64(&bg, &[(init_b, &init_data), (d_out_b, &d_seed)]);
22405 execute_thunks(&sched, arena.raw_buf_mut());
22406 let dinit_b = read_arena_f64(&arena, bg.outputs[1], batch * n);
22407
22408 for i in 0..batch * n {
22409 assert!(
22410 (dinit_b[i] - 1.0).abs() < 1e-12,
22411 "dinit[{i}] = {} (expected 1.0)",
22412 dinit_b[i]
22413 );
22414 }
22415
22416 for bi in 0..batch {
22418 let row = &init_data[bi * n..(bi + 1) * n];
22419 let mut g2 = Graph::new("per_row_grad");
22420 let init2 = g2.input("init", Shape::new(&[n], DType::F64));
22421 let mut body2 = Graph::new("per_row_body");
22422 let c2 = body2.input("carry", Shape::new(&[n], DType::F64));
22423 let ones2_bytes: Vec<u8> = (0..n).flat_map(|_| 1.0_f64.to_le_bytes()).collect();
22424 let ones2 = body2.add_node(
22425 Op::Constant { data: ones2_bytes },
22426 vec![],
22427 Shape::new(&[n], DType::F64),
22428 );
22429 let next2 = body2.binary(BinaryOp::Add, c2, ones2, Shape::new(&[n], DType::F64));
22430 body2.set_outputs(vec![next2]);
22431 let final2 = g2.scan(init2, body2, length);
22432 let loss2 = g2.reduce(
22433 final2,
22434 ReduceOp::Sum,
22435 vec![0],
22436 false,
22437 Shape::new(&[1], DType::F64),
22438 );
22439 g2.set_outputs(vec![loss2]);
22440 let bwd2 = grad_with_loss(&g2, &[init2]);
22441 let init2_id = find(&bwd2, "init");
22442 let d_out2_id = find(&bwd2, "d_output");
22443 let (s2, mut a2) = prepare_f64(&bwd2, &[(init2_id, row), (d_out2_id, &d_seed)]);
22444 execute_thunks(&s2, a2.raw_buf_mut());
22445 let row_dinit = read_arena_f64(&a2, bwd2.outputs[1], n);
22446 for j in 0..n {
22447 let got = dinit_b[bi * n + j];
22448 let want = row_dinit[j];
22449 assert!(
22450 (got - want).abs() < 1e-12,
22451 "row {bi}, j {j}: vmap'd={got} per-row={want}"
22452 );
22453 }
22454 }
22455 }
22456
22457 #[test]
22463 fn vmap_scan_cumulative_sum_matches_scalar_runs() {
22464 use rlx_opt::vmap::vmap;
22465 let n = 2usize;
22466 let length = 4u32;
22467 let batch = 3usize;
22468
22469 let mut body = Graph::new("scan_body_cumsum");
22471 let carry = body.input("carry", Shape::new(&[n], DType::F64));
22472 let x_t = body.input("x_t", Shape::new(&[n], DType::F64));
22473 let next = body.binary(BinaryOp::Add, carry, x_t, Shape::new(&[n], DType::F64));
22474 body.set_outputs(vec![next]);
22475
22476 let mut g = Graph::new("scan_outer_cumsum");
22477 let init = g.input("init", Shape::new(&[n], DType::F64));
22478 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
22479 let final_carry = g.scan_with_xs(init, &[xs], body, length);
22480 g.set_outputs(vec![final_carry]);
22481
22482 let bg = vmap(&g, &["init", "xs"], batch);
22484
22485 let init_data: Vec<f64> = (0..batch * n).map(|i| (i + 1) as f64).collect();
22487 let xs_data: Vec<f64> = (0..batch * length as usize * n)
22490 .map(|i| 0.1 * (i as f64))
22491 .collect();
22492
22493 let find = |graph: &Graph, want: &str| -> NodeId {
22494 for node in graph.nodes() {
22495 if let Op::Input { name } = &node.op
22496 && name == want
22497 {
22498 return node.id;
22499 }
22500 }
22501 panic!("no input {want}");
22502 };
22503 let init_b = find(&bg, "init");
22504 let xs_b = find(&bg, "xs");
22505 let (sched, mut arena) = prepare_f64(&bg, &[(init_b, &init_data), (xs_b, &xs_data)]);
22506 execute_thunks(&sched, arena.raw_buf_mut());
22507 let batched_out = read_arena_f64(&arena, bg.outputs[0], batch * n);
22508
22509 for bi in 0..batch {
22511 let init_slice = &init_data[bi * n..(bi + 1) * n];
22512 let mut x = init_slice.to_vec();
22513 for t in 0..length as usize {
22514 for j in 0..n {
22515 x[j] += xs_data[bi * length as usize * n + t * n + j];
22516 }
22517 }
22518
22519 for i in 0..n {
22520 let got = batched_out[bi * n + i];
22521 assert!(
22522 (got - x[i]).abs() < 1e-12,
22523 "row {bi}, i {i}: got {got} ref {}",
22524 x[i]
22525 );
22526 }
22527 }
22528 }
22529
22530 #[test]
22535 fn vmap_dense_solve_matches_scalar_runs() {
22536 use rlx_opt::vmap::vmap;
22537 let n = 3usize;
22538 let batch = 4usize;
22539
22540 let mut g = Graph::new("solve_forward");
22541 let a = g.input("A", Shape::new(&[n, n], DType::F64));
22542 let b = g.input("b", Shape::new(&[n], DType::F64));
22543 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
22544 g.set_outputs(vec![x]);
22545
22546 let bg = vmap(&g, &["A", "b"], batch);
22548
22549 let mut rng = rlx_ir::Philox4x32::new(0xb47c_u64);
22551 let mut a_data = vec![0.0_f64; batch * n * n];
22552 let mut b_data = vec![0.0_f64; batch * n];
22553 for bi in 0..batch {
22554 for i in 0..n {
22556 for j in 0..n {
22557 a_data[bi * n * n + i * n + j] = rng.next_f32() as f64 * 0.1;
22558 }
22559 a_data[bi * n * n + i * n + i] += 1.0 + n as f64;
22560 }
22561 for i in 0..n {
22562 b_data[bi * n + i] = rng.next_f32() as f64;
22563 }
22564 }
22565
22566 let find = |graph: &Graph, want: &str| -> NodeId {
22567 for node in graph.nodes() {
22568 if let Op::Input { name } = &node.op
22569 && name == want
22570 {
22571 return node.id;
22572 }
22573 }
22574 panic!("no input named {want}");
22575 };
22576 let ba = find(&bg, "A");
22577 let bb = find(&bg, "b");
22578 let (sched, mut arena) = prepare_f64(&bg, &[(ba, &a_data), (bb, &b_data)]);
22579 execute_thunks(&sched, arena.raw_buf_mut());
22580 let batched_x = read_arena_f64(&arena, bg.outputs[0], batch * n);
22581
22582 for bi in 0..batch {
22584 let mut a_slice: Vec<f64> = a_data[bi * n * n..(bi + 1) * n * n].to_vec();
22585 let mut b_slice: Vec<f64> = b_data[bi * n..(bi + 1) * n].to_vec();
22586 crate::blas::dgesv(&mut a_slice, &mut b_slice, n, 1);
22587 for i in 0..n {
22588 let got = batched_x[bi * n + i];
22589 let want = b_slice[i];
22590 assert!(
22591 (got - want).abs() < 1e-12,
22592 "row {bi}, i {i}: got {got} want {want}"
22593 );
22594 }
22595 }
22596 }
22597
22598 #[test]
22605 fn vmap_matmul_add_reduce_matches_scalar_runs() {
22606 use rlx_opt::vmap::vmap;
22607 let n = 3usize;
22608 let batch = 4usize;
22609
22610 let mut g = Graph::new("vmap_e2e_forward");
22612 let x = g.input("x", Shape::new(&[n], DType::F64));
22613 let w = g.input("w", Shape::new(&[n, n], DType::F64));
22614 let b = g.input("b", Shape::new(&[n], DType::F64));
22615 let x_row = g.add_node(
22616 Op::Reshape {
22617 new_shape: vec![1, n as i64],
22618 },
22619 vec![x],
22620 Shape::new(&[1, n], DType::F64),
22621 );
22622 let mm = g.matmul(x_row, w, Shape::new(&[1, n], DType::F64));
22623 let mm_flat = g.add_node(
22624 Op::Reshape {
22625 new_shape: vec![n as i64],
22626 },
22627 vec![mm],
22628 Shape::new(&[n], DType::F64),
22629 );
22630 let yv = g.binary(BinaryOp::Add, mm_flat, b, Shape::new(&[n], DType::F64));
22631 let loss = g.reduce(
22632 yv,
22633 ReduceOp::Sum,
22634 vec![0],
22635 false,
22636 Shape::new(&[1], DType::F64),
22637 );
22638 g.set_outputs(vec![loss]);
22639
22640 let bg = vmap(&g, &["x"], batch);
22642
22643 let mut rng = rlx_ir::Philox4x32::new(0xc1c0_u64);
22645 let n_w = n * n;
22646 let w_data: Vec<f64> = (0..n_w).map(|_| rng.next_f32() as f64).collect();
22647 let b_data: Vec<f64> = (0..n).map(|_| rng.next_f32() as f64).collect();
22648 let mut x_data_batched: Vec<f64> = Vec::with_capacity(batch * n);
22649 for _ in 0..batch * n {
22650 x_data_batched.push(rng.next_f32() as f64);
22651 }
22652
22653 let find = |graph: &Graph, want: &str| -> NodeId {
22655 for node in graph.nodes() {
22656 if let Op::Input { name } = &node.op
22657 && name == want
22658 {
22659 return node.id;
22660 }
22661 }
22662 panic!("no input named {want}");
22663 };
22664 let bx = find(&bg, "x");
22665 let bw = find(&bg, "w");
22666 let bb = find(&bg, "b");
22667 let (sched, mut arena) =
22668 prepare_f64(&bg, &[(bx, &x_data_batched), (bw, &w_data), (bb, &b_data)]);
22669 execute_thunks(&sched, arena.raw_buf_mut());
22670 let batched_out = read_arena_f64(&arena, bg.outputs[0], batch);
22676
22677 for bi in 0..batch {
22679 let xs_slice = &x_data_batched[bi * n..(bi + 1) * n];
22680 let mut g2 = Graph::new("scalar_run");
22681 let x2 = g2.input("x", Shape::new(&[n], DType::F64));
22682 let w2 = g2.input("w", Shape::new(&[n, n], DType::F64));
22683 let b2 = g2.input("b", Shape::new(&[n], DType::F64));
22684 let xr = g2.add_node(
22685 Op::Reshape {
22686 new_shape: vec![1, n as i64],
22687 },
22688 vec![x2],
22689 Shape::new(&[1, n], DType::F64),
22690 );
22691 let m = g2.matmul(xr, w2, Shape::new(&[1, n], DType::F64));
22692 let mf = g2.add_node(
22693 Op::Reshape {
22694 new_shape: vec![n as i64],
22695 },
22696 vec![m],
22697 Shape::new(&[n], DType::F64),
22698 );
22699 let yv2 = g2.binary(BinaryOp::Add, mf, b2, Shape::new(&[n], DType::F64));
22700 let l2 = g2.reduce(
22701 yv2,
22702 ReduceOp::Sum,
22703 vec![0],
22704 false,
22705 Shape::new(&[1], DType::F64),
22706 );
22707 g2.set_outputs(vec![l2]);
22708 let (s2, mut a2) = prepare_f64(&g2, &[(x2, xs_slice), (w2, &w_data), (b2, &b_data)]);
22709 execute_thunks(&s2, a2.raw_buf_mut());
22710 let scalar_out = read_arena_f64(&a2, l2, 1);
22711 assert!(
22712 (batched_out[bi] - scalar_out[0]).abs() < 1e-12,
22713 "row {bi}: batched={} scalar={}",
22714 batched_out[bi],
22715 scalar_out[0]
22716 );
22717 }
22718 }
22719
22720 #[test]
22727 fn scan_with_xs_dxs_matches_fd() {
22728 use rlx_opt::autodiff::grad_with_loss;
22729 let n = 3usize;
22730 let length = 3u32;
22731 let dt = 0.1_f64;
22732
22733 let mut m_data = vec![0.0_f64; n * n];
22734 for i in 0..n {
22735 m_data[i * n + i] = 1.0 + dt * 2.0;
22736 if i > 0 {
22737 m_data[i * n + (i - 1)] = -dt;
22738 }
22739 if i + 1 < n {
22740 m_data[i * n + (i + 1)] = -dt;
22741 }
22742 }
22743 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
22744
22745 let mut body = Graph::new("be_dxs_body");
22746 let carry = body.input("carry", Shape::new(&[n], DType::F64));
22747 let drive = body.input("drive", Shape::new(&[n], DType::F64));
22748 let m = body.add_node(
22749 Op::Constant { data: m_bytes },
22750 vec![],
22751 Shape::new(&[n, n], DType::F64),
22752 );
22753 let driven = body.binary(BinaryOp::Add, carry, drive, Shape::new(&[n], DType::F64));
22754 let next = body.dense_solve(m, driven, Shape::new(&[n], DType::F64));
22755 body.set_outputs(vec![next]);
22756
22757 let mut g = Graph::new("be_dxs_outer");
22758 let init = g.input("init", Shape::new(&[n], DType::F64));
22759 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
22760 let final_carry = g.scan_with_xs(init, &[xs], body, length);
22761 let loss = g.reduce(
22762 final_carry,
22763 ReduceOp::Sum,
22764 vec![0],
22765 false,
22766 Shape::new(&[1], DType::F64),
22767 );
22768 g.set_outputs(vec![loss]);
22769
22770 let bwd = grad_with_loss(&g, &[init, xs]);
22772 assert_eq!(bwd.outputs.len(), 3, "[loss, dinit, dxs]");
22773
22774 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
22775 for node in graph.nodes() {
22776 let name = match &node.op {
22777 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
22778 _ => None,
22779 };
22780 if name == Some(want) {
22781 return node.id;
22782 }
22783 }
22784 panic!("no node named {want:?}");
22785 };
22786 let init_bwd = find_by_name(&bwd, "init");
22787 let xs_bwd = find_by_name(&bwd, "xs");
22788 let d_out_bwd = find_by_name(&bwd, "d_output");
22789
22790 let init_data = vec![0.5_f64, 0.0, -0.5];
22791 let xs_data: Vec<f64> = (0..length as usize * n)
22792 .map(|i| 0.1_f64 * ((i as f64) - 4.0))
22793 .collect();
22794 let d_seed = [1.0_f64];
22795
22796 let (sched, mut arena) = prepare_f64(
22797 &bwd,
22798 &[
22799 (init_bwd, &init_data),
22800 (xs_bwd, &xs_data),
22801 (d_out_bwd, &d_seed),
22802 ],
22803 );
22804 execute_thunks(&sched, arena.raw_buf_mut());
22805 let dinit = read_arena_f64(&arena, bwd.outputs[1], n);
22806 let dxs = read_arena_f64(&arena, bwd.outputs[2], length as usize * n);
22807
22808 let h = 1e-6;
22809 let loss_at = |x0: &[f64], xs_in: &[f64]| -> f64 {
22810 let mut acc = x0.to_vec();
22811 for t in 0..length as usize {
22812 for j in 0..n {
22813 acc[j] += xs_in[t * n + j];
22814 }
22815 let mut a_copy = m_data.clone();
22816 crate::blas::dgesv(&mut a_copy, &mut acc, n, 1);
22817 }
22818 acc.iter().sum()
22819 };
22820
22821 for i in 0..n {
22823 let mut ip = init_data.to_vec();
22824 ip[i] += h;
22825 let mut im = init_data.to_vec();
22826 im[i] -= h;
22827 let fd = (loss_at(&ip, &xs_data) - loss_at(&im, &xs_data)) / (2.0 * h);
22828 assert!(
22829 (dinit[i] - fd).abs() < 1e-7,
22830 "FD dinit[{i}]: AD={} FD={}",
22831 dinit[i],
22832 fd
22833 );
22834 }
22835
22836 for t in 0..length as usize {
22838 for j in 0..n {
22839 let idx = t * n + j;
22840 let mut xp = xs_data.clone();
22841 xp[idx] += h;
22842 let mut xm = xs_data.clone();
22843 xm[idx] -= h;
22844 let fd = (loss_at(&init_data, &xp) - loss_at(&init_data, &xm)) / (2.0 * h);
22845 assert!(
22846 (dxs[idx] - fd).abs() < 1e-7,
22847 "FD dxs[t={t},j={j}]: AD={} FD={}",
22848 dxs[idx],
22849 fd
22850 );
22851 }
22852 }
22853 }
22854
22855 #[test]
22863 fn scan_with_xs_gradient_dinit_matches_fd() {
22864 use rlx_opt::autodiff::grad_with_loss;
22865 let n = 3usize;
22866 let length = 3u32;
22867 let dt = 0.1_f64;
22868
22869 let mut m_data = vec![0.0_f64; n * n];
22870 for i in 0..n {
22871 m_data[i * n + i] = 1.0 + dt * 2.0;
22872 if i > 0 {
22873 m_data[i * n + (i - 1)] = -dt;
22874 }
22875 if i + 1 < n {
22876 m_data[i * n + (i + 1)] = -dt;
22877 }
22878 }
22879 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
22880
22881 let mut body = Graph::new("be_xs_grad_body");
22882 let carry = body.input("carry", Shape::new(&[n], DType::F64));
22883 let drive = body.input("drive", Shape::new(&[n], DType::F64));
22884 let m = body.add_node(
22885 Op::Constant { data: m_bytes },
22886 vec![],
22887 Shape::new(&[n, n], DType::F64),
22888 );
22889 let driven = body.binary(BinaryOp::Add, carry, drive, Shape::new(&[n], DType::F64));
22890 let next = body.dense_solve(m, driven, Shape::new(&[n], DType::F64));
22891 body.set_outputs(vec![next]);
22892
22893 let mut g = Graph::new("be_xs_grad_outer");
22894 let init = g.input("init", Shape::new(&[n], DType::F64));
22895 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
22896 let final_carry = g.scan_with_xs(init, &[xs], body, length);
22897 let loss = g.reduce(
22898 final_carry,
22899 ReduceOp::Sum,
22900 vec![0],
22901 false,
22902 Shape::new(&[1], DType::F64),
22903 );
22904 g.set_outputs(vec![loss]);
22905
22906 let bwd = grad_with_loss(&g, &[init]);
22907
22908 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
22909 for node in graph.nodes() {
22910 let name = match &node.op {
22911 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
22912 _ => None,
22913 };
22914 if name == Some(want) {
22915 return node.id;
22916 }
22917 }
22918 panic!("no node named {want:?}");
22919 };
22920 let init_bwd = find_by_name(&bwd, "init");
22921 let xs_bwd = find_by_name(&bwd, "xs");
22922 let d_out_bwd = find_by_name(&bwd, "d_output");
22923
22924 let init_data = vec![0.5_f64, 0.0, -0.5];
22925 let xs_data: Vec<f64> = (0..length as usize * n)
22927 .map(|i| 0.1_f64 * ((i as f64) - 4.0))
22928 .collect();
22929 let d_seed = [1.0_f64];
22930
22931 let (sched, mut arena) = prepare_f64(
22932 &bwd,
22933 &[
22934 (init_bwd, &init_data),
22935 (xs_bwd, &xs_data),
22936 (d_out_bwd, &d_seed),
22937 ],
22938 );
22939 execute_thunks(&sched, arena.raw_buf_mut());
22940 let dinit = read_arena_f64(&arena, bwd.outputs[1], n);
22941
22942 let h = 1e-6;
22943 let loss_at = |x0: &[f64]| -> f64 {
22944 let mut acc = x0.to_vec();
22945 for t in 0..length as usize {
22946 for j in 0..n {
22947 acc[j] += xs_data[t * n + j];
22948 }
22949 let mut a_copy = m_data.clone();
22950 crate::blas::dgesv(&mut a_copy, &mut acc, n, 1);
22951 }
22952 acc.iter().sum()
22953 };
22954 for i in 0..n {
22955 let mut ip = init_data.to_vec();
22956 ip[i] += h;
22957 let mut im = init_data.to_vec();
22958 im[i] -= h;
22959 let fd = (loss_at(&ip) - loss_at(&im)) / (2.0 * h);
22960 assert!(
22961 (dinit[i] - fd).abs() < 1e-7,
22962 "FD dinit[{i}]: AD={} FD={}",
22963 dinit[i],
22964 fd
22965 );
22966 }
22967 }
22968
22969 #[test]
22977 fn scan_gradient_geometric_matches_closed_form() {
22978 use rlx_opt::autodiff::grad_with_loss;
22979 let n = 3usize;
22980 let length = 5u32;
22981
22982 let mut body = Graph::new("scan_grad_body");
22983 let x = body.input("carry", Shape::new(&[n], DType::F64));
22984 let scale_bytes: Vec<u8> = (0..n).flat_map(|_| 1.1_f64.to_le_bytes()).collect();
22985 let scale = body.add_node(
22986 Op::Constant { data: scale_bytes },
22987 vec![],
22988 Shape::new(&[n], DType::F64),
22989 );
22990 let next = body.binary(BinaryOp::Mul, x, scale, Shape::new(&[n], DType::F64));
22991 body.set_outputs(vec![next]);
22992
22993 let mut g = Graph::new("scan_grad_outer");
22994 let init = g.input("init", Shape::new(&[n], DType::F64));
22995 let final_x = g.scan(init, body, length);
22996 let loss = g.reduce(
22997 final_x,
22998 ReduceOp::Sum,
22999 vec![0],
23000 false,
23001 Shape::new(&[1], DType::F64),
23002 );
23003 g.set_outputs(vec![loss]);
23004
23005 let bwd = grad_with_loss(&g, &[init]);
23006 assert_eq!(bwd.outputs.len(), 2);
23007
23008 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
23009 for node in graph.nodes() {
23010 let name = match &node.op {
23011 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
23012 _ => None,
23013 };
23014 if name == Some(want) {
23015 return node.id;
23016 }
23017 }
23018 panic!("no node named {want:?}");
23019 };
23020 let init_bwd = find_by_name(&bwd, "init");
23021 let d_out_bwd = find_by_name(&bwd, "d_output");
23022
23023 let init_data = vec![1.0_f64; n];
23024 let d_seed = [1.0_f64];
23025 let (sched, mut arena) = prepare_f64(&bwd, &[(init_bwd, &init_data), (d_out_bwd, &d_seed)]);
23026 execute_thunks(&sched, arena.raw_buf_mut());
23027 let dinit = read_arena_f64(&arena, bwd.outputs[1], n);
23028
23029 let want = 1.1_f64.powi(length as i32);
23030 for i in 0..n {
23031 assert!(
23032 (dinit[i] - want).abs() < 1e-12,
23033 "dinit[{i}] = {} want {}",
23034 dinit[i],
23035 want
23036 );
23037 }
23038
23039 let h = 1e-6;
23041 let loss_at = |x: &[f64]| -> f64 {
23042 let mut acc = x.to_vec();
23043 for _ in 0..length {
23044 for v in acc.iter_mut() {
23045 *v *= 1.1;
23046 }
23047 }
23048 acc.iter().sum()
23049 };
23050 let mut ip = init_data.clone();
23051 ip[0] += h;
23052 let mut im = init_data.clone();
23053 im[0] -= h;
23054 let fd = (loss_at(&ip) - loss_at(&im)) / (2.0 * h);
23055 assert!(
23056 (dinit[0] - fd).abs() < 1e-7,
23057 "FD dinit[0]: AD={} FD={}",
23058 dinit[0],
23059 fd
23060 );
23061 }
23062
23063 #[test]
23066 fn scan_gradient_backward_euler_matches_fd() {
23067 use rlx_opt::autodiff::grad_with_loss;
23068 let n = 4usize;
23069 let length = 3u32;
23070 let dt = 0.05_f64;
23071
23072 let mut m_data = vec![0.0_f64; n * n];
23073 for i in 0..n {
23074 m_data[i * n + i] = 1.0 + dt * 2.0;
23075 if i > 0 {
23076 m_data[i * n + (i - 1)] = -dt;
23077 }
23078 if i + 1 < n {
23079 m_data[i * n + (i + 1)] = -dt;
23080 }
23081 }
23082 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
23083
23084 let mut body = Graph::new("be_grad_body");
23085 let x = body.input("x", Shape::new(&[n], DType::F64));
23086 let m = body.add_node(
23087 Op::Constant { data: m_bytes },
23088 vec![],
23089 Shape::new(&[n, n], DType::F64),
23090 );
23091 let next = body.dense_solve(m, x, Shape::new(&[n], DType::F64));
23092 body.set_outputs(vec![next]);
23093
23094 let mut g = Graph::new("be_grad_outer");
23095 let init = g.input("x0", Shape::new(&[n], DType::F64));
23096 let final_x = g.scan(init, body, length);
23097 let loss = g.reduce(
23098 final_x,
23099 ReduceOp::Sum,
23100 vec![0],
23101 false,
23102 Shape::new(&[1], DType::F64),
23103 );
23104 g.set_outputs(vec![loss]);
23105
23106 let bwd = grad_with_loss(&g, &[init]);
23107
23108 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
23109 for node in graph.nodes() {
23110 let name = match &node.op {
23111 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
23112 _ => None,
23113 };
23114 if name == Some(want) {
23115 return node.id;
23116 }
23117 }
23118 panic!("no node named {want:?}");
23119 };
23120 let init_bwd = find_by_name(&bwd, "x0");
23121 let d_out_bwd = find_by_name(&bwd, "d_output");
23122
23123 let init_data: [f64; 4] = [0.0, 1.0, 0.0, 0.0];
23124 let d_seed = [1.0_f64];
23125 let (sched, mut arena) = prepare_f64(&bwd, &[(init_bwd, &init_data), (d_out_bwd, &d_seed)]);
23126 execute_thunks(&sched, arena.raw_buf_mut());
23127 let dinit = read_arena_f64(&arena, bwd.outputs[1], n);
23128
23129 let h = 1e-6;
23130 let loss_at = |x0: &[f64]| -> f64 {
23131 let mut acc = x0.to_vec();
23132 for _ in 0..length {
23133 let mut a_copy = m_data.clone();
23134 crate::blas::dgesv(&mut a_copy, &mut acc, n, 1);
23135 }
23136 acc.iter().sum()
23137 };
23138 for i in 0..n {
23139 let mut ip = init_data.to_vec();
23140 ip[i] += h;
23141 let mut im = init_data.to_vec();
23142 im[i] -= h;
23143 let fd = (loss_at(&ip) - loss_at(&im)) / (2.0 * h);
23144 assert!(
23145 (dinit[i] - fd).abs() < 1e-7,
23146 "FD dinit[{i}]: AD={} FD={}",
23147 dinit[i],
23148 fd
23149 );
23150 }
23151 }
23152
23153 #[test]
23159 fn scan_trajectory_backward_euler_records_waveform() {
23160 let n = 4usize;
23161 let length = 5u32;
23162 let dt = 0.05_f64;
23163
23164 let mut m_data = vec![0.0_f64; n * n];
23165 for i in 0..n {
23166 m_data[i * n + i] = 1.0 + dt * 2.0;
23167 if i > 0 {
23168 m_data[i * n + (i - 1)] = -dt;
23169 }
23170 if i + 1 < n {
23171 m_data[i * n + (i + 1)] = -dt;
23172 }
23173 }
23174 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
23175
23176 let mut body = Graph::new("be_traj_body");
23177 let x = body.input("x", Shape::new(&[n], DType::F64));
23178 let m = body.add_node(
23179 Op::Constant { data: m_bytes },
23180 vec![],
23181 Shape::new(&[n, n], DType::F64),
23182 );
23183 let next = body.dense_solve(m, x, Shape::new(&[n], DType::F64));
23184 body.set_outputs(vec![next]);
23185
23186 let mut g = Graph::new("be_traj_outer");
23187 let init = g.input("x0", Shape::new(&[n], DType::F64));
23188 let traj = g.scan_trajectory(init, body, length);
23189 g.set_outputs(vec![traj]);
23190
23191 let init_data: [f64; 4] = [0.0, 1.0, 0.0, 0.0];
23192 let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data)]);
23193 execute_thunks(&sched, arena.raw_buf_mut());
23194 let got = read_arena_f64(&arena, traj, length as usize * n);
23195
23196 let mut want = Vec::<f64>::with_capacity(length as usize * n);
23198 let mut x_ref = init_data.to_vec();
23199 for _ in 0..length {
23200 let mut a_copy = m_data.clone();
23201 crate::blas::dgesv(&mut a_copy, &mut x_ref, n, 1);
23202 want.extend_from_slice(&x_ref);
23203 }
23204 for i in 0..length as usize * n {
23205 assert!(
23206 (got[i] - want[i]).abs() < 1e-12,
23207 "got[{i}] = {} ref {}",
23208 got[i],
23209 want[i]
23210 );
23211 }
23212
23213 for t in 1..length as usize {
23216 let prev: f64 = got[(t - 1) * n..t * n].iter().sum();
23217 let curr: f64 = got[t * n..(t + 1) * n].iter().sum();
23218 assert!(
23219 curr <= prev + 1e-15,
23220 "mass should decay: row {} sum {prev}, row {t} sum {curr}",
23221 t - 1
23222 );
23223 }
23224
23225 let mut body2 = Graph::new("be_final_body");
23229 let x2 = body2.input("x", Shape::new(&[n], DType::F64));
23230 let m_bytes2: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
23231 let m2 = body2.add_node(
23232 Op::Constant { data: m_bytes2 },
23233 vec![],
23234 Shape::new(&[n, n], DType::F64),
23235 );
23236 let next2 = body2.dense_solve(m2, x2, Shape::new(&[n], DType::F64));
23237 body2.set_outputs(vec![next2]);
23238
23239 let mut g2 = Graph::new("be_final_outer");
23240 let init2 = g2.input("x0", Shape::new(&[n], DType::F64));
23241 let final_x = g2.scan(init2, body2, length);
23242 g2.set_outputs(vec![final_x]);
23243 let (sched2, mut arena2) = prepare_f64(&g2, &[(init2, &init_data)]);
23244 execute_thunks(&sched2, arena2.raw_buf_mut());
23245 let final_got = read_arena_f64(&arena2, final_x, n);
23246
23247 let last_row = &got[(length as usize - 1) * n..length as usize * n];
23248 for i in 0..n {
23249 assert!(
23250 (last_row[i] - final_got[i]).abs() < 1e-15,
23251 "last trajectory row[{i}] = {} vs final-scan = {}",
23252 last_row[i],
23253 final_got[i]
23254 );
23255 }
23256 }
23257
23258 #[test]
23264 fn scan_backward_euler_heat_f64() {
23265 let n = 4usize;
23266 let length = 5u32;
23267 let dt = 0.05_f64;
23268
23269 let mut m_data = vec![0.0_f64; n * n];
23272 for i in 0..n {
23273 m_data[i * n + i] = 1.0 + dt * 2.0;
23274 if i > 0 {
23275 m_data[i * n + (i - 1)] = -dt;
23276 }
23277 if i + 1 < n {
23278 m_data[i * n + (i + 1)] = -dt;
23279 }
23280 }
23281 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
23282
23283 let mut body = Graph::new("be_body");
23284 let x = body.input("x", Shape::new(&[n], DType::F64));
23285 let m = body.add_node(
23286 Op::Constant { data: m_bytes },
23287 vec![],
23288 Shape::new(&[n, n], DType::F64),
23289 );
23290 let next = body.dense_solve(m, x, Shape::new(&[n], DType::F64));
23291 body.set_outputs(vec![next]);
23292
23293 let mut g = Graph::new("be_outer");
23294 let init = g.input("x0", Shape::new(&[n], DType::F64));
23295 let final_x = g.scan(init, body, length);
23296 g.set_outputs(vec![final_x]);
23297
23298 let init_data: [f64; 4] = [0.0, 1.0, 0.0, 0.0];
23300 let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data)]);
23301 execute_thunks(&sched, arena.raw_buf_mut());
23302 let got = read_arena_f64(&arena, final_x, n);
23303
23304 let mut ref_x = init_data.to_vec();
23306 for _ in 0..length {
23307 let mut a_copy = m_data.clone();
23308 crate::blas::dgesv(&mut a_copy, &mut ref_x, n, 1);
23309 }
23310 for i in 0..n {
23311 assert!(
23312 (got[i] - ref_x[i]).abs() < 1e-12,
23313 "got[{i}] = {} ref {}",
23314 got[i],
23315 ref_x[i]
23316 );
23317 }
23318 let mass: f64 = got.iter().sum();
23323 assert!(mass > 0.0 && mass < 1.0, "diffusion mass: {mass}");
23324 }
23325
23326 #[test]
23330 fn dense_solve_f64_multi_rhs_forward() {
23331 let n = 3usize;
23332 let k = 2usize;
23333 let mut g = Graph::new("solve_multi_rhs");
23334 let a = g.input("A", Shape::new(&[n, n], DType::F64));
23335 let b = g.input("B", Shape::new(&[n, k], DType::F64));
23336 let x = g.dense_solve(a, b, Shape::new(&[n, k], DType::F64));
23337 g.set_outputs(vec![x]);
23338
23339 let a_data = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0_f64];
23340 let b_data = [1.0, 4.0, 2.0, -1.0, 3.0, 2.0_f64];
23341 let (sched, mut arena) = prepare_f64(&g, &[(a, &a_data), (b, &b_data)]);
23342 execute_thunks(&sched, arena.raw_buf_mut());
23343 let x_got = read_arena_f64(&arena, x, n * k);
23344 for c in 0..k {
23345 for i in 0..n {
23346 let mut acc = 0.0_f64;
23347 for j in 0..n {
23348 acc += a_data[i * n + j] * x_got[j * k + c];
23349 }
23350 let want = b_data[i * k + c];
23351 assert!(
23352 (acc - want).abs() < 1e-10,
23353 "col {c} row {i}: got {acc} want {want}"
23354 );
23355 }
23356 }
23357 }
23358
23359 #[test]
23362 fn dense_solve_f64_multi_rhs_gradient() {
23363 use rlx_opt::autodiff::grad_with_loss;
23364 let n = 3usize;
23365 let k = 2usize;
23366 let mut g = Graph::new("solve_mrhs_grad");
23367 let a = g.param("A", Shape::new(&[n, n], DType::F64));
23368 let b = g.input("B", Shape::new(&[n, k], DType::F64));
23369 let x = g.dense_solve(a, b, Shape::new(&[n, k], DType::F64));
23370 let loss = g.reduce(
23371 x,
23372 ReduceOp::Sum,
23373 vec![0, 1],
23374 false,
23375 Shape::new(&[1], DType::F64),
23376 );
23377 g.set_outputs(vec![loss]);
23378
23379 let bwd = grad_with_loss(&g, &[a, b]);
23380 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
23381 for node in graph.nodes() {
23382 let name = match &node.op {
23383 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
23384 _ => None,
23385 };
23386 if name == Some(want) {
23387 return node.id;
23388 }
23389 }
23390 panic!("no node named {want:?}");
23391 };
23392 let a_bwd = find_by_name(&bwd, "A");
23393 let b_bwd = find_by_name(&bwd, "B");
23394 let d_out = find_by_name(&bwd, "d_output");
23395
23396 let a_data = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0_f64];
23397 let b_data = [1.0, 4.0, 2.0, -1.0, 3.0, 2.0_f64];
23398 let d_seed = [1.0_f64];
23399
23400 let (sched, mut arena) = prepare_f64(
23401 &bwd,
23402 &[(a_bwd, &a_data), (b_bwd, &b_data), (d_out, &d_seed)],
23403 );
23404 execute_thunks(&sched, arena.raw_buf_mut());
23405 let da_got = read_arena_f64(&arena, bwd.outputs[1], n * n);
23406 let db_got = read_arena_f64(&arena, bwd.outputs[2], n * k);
23407
23408 let mut x_ref = b_data;
23410 {
23411 let mut a_copy = a_data;
23412 crate::blas::dgesv(&mut a_copy, &mut x_ref, n, k);
23413 }
23414 let mut at = [0.0_f64; 9];
23415 for i in 0..n {
23416 for j in 0..n {
23417 at[i * n + j] = a_data[j * n + i];
23418 }
23419 }
23420 let mut ones_nk = vec![1.0_f64; n * k];
23421 crate::blas::dgesv(&mut at, &mut ones_nk, n, k);
23422 let db_ref = ones_nk;
23423 let mut da_ref = [0.0_f64; 9];
23424 for i in 0..n {
23425 for j in 0..n {
23426 let mut acc = 0.0_f64;
23427 for c in 0..k {
23428 acc += db_ref[i * k + c] * x_ref[j * k + c];
23429 }
23430 da_ref[i * n + j] = -acc;
23431 }
23432 }
23433 for i in 0..n * k {
23434 assert!(
23435 (db_got[i] - db_ref[i]).abs() < 1e-10,
23436 "dB[{i}]: got {} want {}",
23437 db_got[i],
23438 db_ref[i]
23439 );
23440 }
23441 for i in 0..n * n {
23442 assert!(
23443 (da_got[i] - da_ref[i]).abs() < 1e-10,
23444 "dA[{i}]: got {} want {}",
23445 da_got[i],
23446 da_ref[i]
23447 );
23448 }
23449
23450 let h = 1e-6;
23452 let mut bp = b_data;
23453 bp[0] += h;
23454 let mut bm = b_data;
23455 bm[0] -= h;
23456 let xp = {
23457 let mut a_copy = a_data;
23458 crate::blas::dgesv(&mut a_copy, &mut bp, n, k);
23459 bp
23460 };
23461 let xm = {
23462 let mut a_copy = a_data;
23463 crate::blas::dgesv(&mut a_copy, &mut bm, n, k);
23464 bm
23465 };
23466 let lp: f64 = xp.iter().sum();
23467 let lm: f64 = xm.iter().sum();
23468 let fd = (lp - lm) / (2.0 * h);
23469 assert!(
23470 (db_got[0] - fd).abs() < 1e-7,
23471 "FD dB[0,0]: AD={} FD={}",
23472 db_got[0],
23473 fd
23474 );
23475 }
23476
23477 #[test]
23479 fn dense_solve_f64_multi_rhs_jvp() {
23480 use rlx_opt::autodiff_fwd::jvp;
23481 let n = 3usize;
23482 let k = 2usize;
23483 let mut g = Graph::new("solve_mrhs_jvp");
23484 let a = g.input("A", Shape::new(&[n, n], DType::F64));
23485 let b = g.input("B", Shape::new(&[n, k], DType::F64));
23486 let x = g.dense_solve(a, b, Shape::new(&[n, k], DType::F64));
23487 g.set_outputs(vec![x]);
23488
23489 let jg = jvp(&g, &[b]);
23490 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
23491 for node in graph.nodes() {
23492 let name = match &node.op {
23493 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
23494 _ => None,
23495 };
23496 if name == Some(want) {
23497 return node.id;
23498 }
23499 }
23500 panic!("no node named {want:?}");
23501 };
23502 let a_id = find_by_name(&jg, "A");
23503 let b_id = find_by_name(&jg, "B");
23504 let tb_id = find_by_name(&jg, "tangent_B");
23505
23506 let a_data = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0_f64];
23507 let b_data = [1.0, 4.0, 2.0, -1.0, 3.0, 2.0_f64];
23508 let tb_data = [0.5, 0.0, -0.25, 1.0, 1.0, -0.5_f64];
23509
23510 let (sched, mut arena) =
23511 prepare_f64(&jg, &[(a_id, &a_data), (b_id, &b_data), (tb_id, &tb_data)]);
23512 execute_thunks(&sched, arena.raw_buf_mut());
23513 let tangent_x = read_arena_f64(&arena, jg.outputs[1], n * k);
23514
23515 let mut a_copy = a_data;
23516 let mut tb_copy = tb_data;
23517 crate::blas::dgesv(&mut a_copy, &mut tb_copy, n, k);
23518 for i in 0..n * k {
23519 assert!(
23520 (tangent_x[i] - tb_copy[i]).abs() < 1e-10,
23521 "t_X[{i}]: AD={} ref={}",
23522 tangent_x[i],
23523 tb_copy[i]
23524 );
23525 }
23526
23527 let h = 1e-6;
23528 let mut bp = b_data;
23529 let mut bm = b_data;
23530 for i in 0..n * k {
23531 bp[i] += h * tb_data[i];
23532 bm[i] -= h * tb_data[i];
23533 }
23534 let xp = {
23535 let mut a_copy = a_data;
23536 crate::blas::dgesv(&mut a_copy, &mut bp, n, k);
23537 bp
23538 };
23539 let xm = {
23540 let mut a_copy = a_data;
23541 crate::blas::dgesv(&mut a_copy, &mut bm, n, k);
23542 bm
23543 };
23544 for i in 0..n * k {
23545 let fd = (xp[i] - xm[i]) / (2.0 * h);
23546 assert!(
23547 (tangent_x[i] - fd).abs() < 1e-7,
23548 "FD t_X[{i}]: AD={} FD={}",
23549 tangent_x[i],
23550 fd
23551 );
23552 }
23553 }
23554
23555 #[test]
23562 fn jvp_dense_solve_b_runs_and_matches_fd() {
23563 use rlx_opt::autodiff_fwd::jvp;
23564 let n = 3usize;
23565
23566 let mut g = Graph::new("jvp_b_e2e");
23568 let a = g.input("A", Shape::new(&[n, n], DType::F64));
23569 let b = g.input("b", Shape::new(&[n], DType::F64));
23570 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
23571 g.set_outputs(vec![x]);
23572
23573 let jg = jvp(&g, &[b]);
23575 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
23577 for node in graph.nodes() {
23578 let name = match &node.op {
23579 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
23580 _ => None,
23581 };
23582 if name == Some(want) {
23583 return node.id;
23584 }
23585 }
23586 panic!("no node named {want:?}");
23587 };
23588 let a_id = find_by_name(&jg, "A");
23589 let b_id = find_by_name(&jg, "b");
23590 let tb_id = find_by_name(&jg, "tangent_b");
23591
23592 let a_data: [f64; 9] = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0];
23593 let b_data: [f64; 3] = [1.0, 2.0, 3.0];
23594 let tb_data: [f64; 3] = [0.5, -0.25, 1.0];
23596
23597 let (sched, mut arena) =
23598 prepare_f64(&jg, &[(a_id, &a_data), (b_id, &b_data), (tb_id, &tb_data)]);
23599 execute_thunks(&sched, arena.raw_buf_mut());
23600
23601 let primal_x = read_arena_f64(&arena, jg.outputs[0], n);
23603 let tangent_x = read_arena_f64(&arena, jg.outputs[1], n);
23604
23605 let t_x_ref = {
23607 let mut a = a_data;
23608 let mut tb = tb_data;
23609 let info = crate::blas::dgesv(&mut a, &mut tb, n, 1);
23610 assert_eq!(info, 0);
23611 tb
23612 };
23613 for i in 0..n {
23614 assert!(
23615 (tangent_x[i] - t_x_ref[i]).abs() < 1e-10,
23616 "t_x[{i}]: got {} want {}",
23617 tangent_x[i],
23618 t_x_ref[i]
23619 );
23620 }
23621
23622 let h = 1e-6;
23624 let mut bp = b_data;
23625 let mut bm = b_data;
23626 for i in 0..n {
23627 bp[i] += h * tb_data[i];
23628 bm[i] -= h * tb_data[i];
23629 }
23630 let xp = {
23631 let mut a = a_data;
23632 let info = crate::blas::dgesv(&mut a, &mut bp, n, 1);
23633 assert_eq!(info, 0);
23634 bp
23635 };
23636 let xm = {
23637 let mut a = a_data;
23638 let info = crate::blas::dgesv(&mut a, &mut bm, n, 1);
23639 assert_eq!(info, 0);
23640 bm
23641 };
23642 let fd: Vec<f64> = (0..n).map(|i| (xp[i] - xm[i]) / (2.0 * h)).collect();
23643 for i in 0..n {
23644 assert!(
23645 (tangent_x[i] - fd[i]).abs() < 1e-7,
23646 "FD mismatch t_x[{i}]: AD={} FD={}",
23647 tangent_x[i],
23648 fd[i]
23649 );
23650 }
23651 let primal_ref = {
23653 let mut a = a_data;
23654 let mut b = b_data;
23655 crate::blas::dgesv(&mut a, &mut b, n, 1);
23656 b
23657 };
23658 for i in 0..n {
23659 assert!((primal_x[i] - primal_ref[i]).abs() < 1e-10);
23660 }
23661 }
23662
23663 #[test]
23669 fn jvp_dense_solve_a_runs_and_matches_fd() {
23670 use rlx_opt::autodiff_fwd::jvp;
23671 let n = 3usize;
23672
23673 let mut g = Graph::new("jvp_a_e2e");
23674 let a = g.input("A", Shape::new(&[n, n], DType::F64));
23675 let b = g.input("b", Shape::new(&[n], DType::F64));
23676 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
23677 g.set_outputs(vec![x]);
23678
23679 let jg = jvp(&g, &[a]);
23680 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
23681 for node in graph.nodes() {
23682 let name = match &node.op {
23683 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
23684 _ => None,
23685 };
23686 if name == Some(want) {
23687 return node.id;
23688 }
23689 }
23690 panic!("no node named {want:?}");
23691 };
23692 let a_id = find_by_name(&jg, "A");
23693 let b_id = find_by_name(&jg, "b");
23694 let ta_id = find_by_name(&jg, "tangent_A");
23695
23696 let a_data: [f64; 9] = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0];
23697 let b_data: [f64; 3] = [1.0, 2.0, 3.0];
23698 let ta_data: [f64; 9] = [0.10, -0.05, 0.02, 0.03, 0.20, -0.04, -0.01, 0.07, 0.15];
23700
23701 let (sched, mut arena) =
23702 prepare_f64(&jg, &[(a_id, &a_data), (b_id, &b_data), (ta_id, &ta_data)]);
23703 execute_thunks(&sched, arena.raw_buf_mut());
23704
23705 let tangent_x = read_arena_f64(&arena, jg.outputs[1], n);
23706
23707 let x_ref = {
23709 let mut a = a_data;
23710 let mut b = b_data;
23711 crate::blas::dgesv(&mut a, &mut b, n, 1);
23712 b
23713 };
23714 let mut prod = [0.0_f64; 3];
23715 for i in 0..n {
23716 for j in 0..n {
23717 prod[i] += ta_data[i * n + j] * x_ref[j];
23718 }
23719 }
23720 let t_x_ref = {
23721 let mut a = a_data;
23722 let mut p = prod;
23723 crate::blas::dgesv(&mut a, &mut p, n, 1);
23724 [-p[0], -p[1], -p[2]]
23725 };
23726 for i in 0..n {
23727 assert!(
23728 (tangent_x[i] - t_x_ref[i]).abs() < 1e-10,
23729 "closed-form t_x[{i}]: AD={} ref={}",
23730 tangent_x[i],
23731 t_x_ref[i]
23732 );
23733 }
23734
23735 let h = 1e-6;
23737 let mut ap = a_data;
23738 let mut am = a_data;
23739 for i in 0..n * n {
23740 ap[i] += h * ta_data[i];
23741 am[i] -= h * ta_data[i];
23742 }
23743 let xp = {
23744 let mut a = ap;
23745 let mut b = b_data;
23746 crate::blas::dgesv(&mut a, &mut b, n, 1);
23747 b
23748 };
23749 let xm = {
23750 let mut a = am;
23751 let mut b = b_data;
23752 crate::blas::dgesv(&mut a, &mut b, n, 1);
23753 b
23754 };
23755 for i in 0..n {
23756 let fd = (xp[i] - xm[i]) / (2.0 * h);
23757 assert!(
23758 (tangent_x[i] - fd).abs() < 1e-7,
23759 "FD t_x[{i}]: AD={} FD={}",
23760 tangent_x[i],
23761 fd
23762 );
23763 }
23764 }
23765
23766 #[test]
23772 fn q_conv2d_matches_reference() {
23773 use rlx_ir::Philox4x32;
23774 let n = 1usize;
23776 let c_in = 2usize;
23777 let h = 5usize;
23778 let w_in = 5usize;
23779 let c_out = 3usize;
23780 let kh = 3usize;
23781 let kw = 3usize;
23782 let ph = 1usize;
23783 let pw = 1usize;
23784 let sh = 1usize;
23785 let sw = 1usize;
23786 let h_out = (h + 2 * ph - kh) / sh + 1;
23787 let w_out = (w_in + 2 * pw - kw) / sw + 1;
23788
23789 let x_scale = 0.04f32;
23790 let w_scale = 0.02f32;
23791 let out_scale = 0.5f32;
23792 let mult = x_scale * w_scale / out_scale;
23793
23794 let mut rng = Philox4x32::new(2099);
23795 let mut xf = vec![0f32; n * c_in * h * w_in];
23796 rng.fill_normal(&mut xf);
23797 let mut wf = vec![0f32; c_out * c_in * kh * kw];
23798 rng.fill_normal(&mut wf);
23799 let xq: Vec<i8> = xf
23800 .iter()
23801 .map(|&v| ((v / x_scale).round() as i32).clamp(-128, 127) as i8)
23802 .collect();
23803 let wq: Vec<i8> = wf
23804 .iter()
23805 .map(|&v| ((v / w_scale).round() as i32).clamp(-128, 127) as i8)
23806 .collect();
23807 let bias: Vec<i32> = vec![0i32; c_out];
23808
23809 let mut g = Graph::new("qconv");
23810 let xn = g.input("x", Shape::new(&[n, c_in, h, w_in], DType::I8));
23811 let wn = g.input("w", Shape::new(&[c_out, c_in, kh, kw], DType::I8));
23812 let bn = g.input("b", Shape::new(&[c_out], DType::I32));
23813 let out = g.q_conv2d(
23814 xn,
23815 wn,
23816 bn,
23817 vec![kh, kw],
23818 vec![sh, sw],
23819 vec![ph, pw],
23820 vec![1, 1],
23821 1,
23822 0,
23823 0,
23824 0,
23825 mult,
23826 Shape::new(&[n, c_out, h_out, w_out], DType::I8),
23827 );
23828 g.set_outputs(vec![out]);
23829
23830 let plan = rlx_opt::memory::plan_memory(&g);
23831 let mut arena = crate::arena::Arena::from_plan(plan);
23832 let sched = compile_thunks(&g, &arena);
23833 let xn_off = arena.byte_offset(xn);
23836 let wn_off = arena.byte_offset(wn);
23837 let bn_off = arena.byte_offset(bn);
23838 let out_off = arena.byte_offset(out);
23839 let buf = arena.raw_buf_mut();
23840 unsafe {
23841 let p = buf.as_mut_ptr().add(xn_off) as *mut i8;
23842 for (i, &v) in xq.iter().enumerate() {
23843 *p.add(i) = v;
23844 }
23845 let p = buf.as_mut_ptr().add(wn_off) as *mut i8;
23846 for (i, &v) in wq.iter().enumerate() {
23847 *p.add(i) = v;
23848 }
23849 let p = buf.as_mut_ptr().add(bn_off) as *mut i32;
23850 for (i, &v) in bias.iter().enumerate() {
23851 *p.add(i) = v;
23852 }
23853 }
23854 execute_thunks(&sched, arena.raw_buf_mut());
23855 let out_q: Vec<i8> = unsafe {
23856 let p = arena.raw_buf().as_ptr().add(out_off) as *const i8;
23857 (0..n * c_out * h_out * w_out).map(|i| *p.add(i)).collect()
23858 };
23859
23860 let mut out_ref = vec![0i8; n * c_out * h_out * w_out];
23862 for ni in 0..n {
23863 for co in 0..c_out {
23864 for ho in 0..h_out {
23865 for wo in 0..w_out {
23866 let mut acc: i32 = 0;
23867 for ci in 0..c_in {
23868 for ki in 0..kh {
23869 for kj in 0..kw {
23870 let hi = ho * sh + ki;
23871 let wi = wo * sw + kj;
23872 if hi < ph || wi < pw {
23873 continue;
23874 }
23875 let hi = hi - ph;
23876 let wi = wi - pw;
23877 if hi >= h || wi >= w_in {
23878 continue;
23879 }
23880 let xv =
23881 xq[((ni * c_in) + ci) * h * w_in + hi * w_in + wi] as i32;
23882 let wv = wq[((co * c_in) + ci) * kh * kw + ki * kw + kj] as i32;
23883 acc += xv * wv;
23884 }
23885 }
23886 }
23887 let r = (acc as f32 * mult).round() as i32;
23888 let r = r.clamp(-128, 127) as i8;
23889 out_ref[((ni * c_out) + co) * h_out * w_out + ho * w_out + wo] = r;
23890 }
23891 }
23892 }
23893 }
23894
23895 for (i, (a, r)) in out_q.iter().zip(&out_ref).enumerate() {
23896 assert_eq!(a, r, "q_conv2d[{i}]: kernel {a} vs reference {r}");
23897 }
23898 }
23899
23900 #[test]
23908 fn q_matmul_matches_fake_quant_reference() {
23909 use rlx_ir::Philox4x32;
23910 let m = 3usize;
23911 let k = 8usize;
23912 let n = 5usize;
23913 let mut rng = Philox4x32::new(2031);
23914
23915 let x_scale = 0.05f32;
23917 let w_scale = 0.03f32;
23918 let out_scale = 0.4f32;
23919 let mult = x_scale * w_scale / out_scale;
23920 let mut xf = vec![0f32; m * k];
23921 rng.fill_normal(&mut xf);
23922 let mut wf = vec![0f32; k * n];
23923 rng.fill_normal(&mut wf);
23924 let xq: Vec<i8> = xf
23925 .iter()
23926 .map(|&v| ((v / x_scale).round() as i32).clamp(-128, 127) as i8)
23927 .collect();
23928 let wq: Vec<i8> = wf
23929 .iter()
23930 .map(|&v| ((v / w_scale).round() as i32).clamp(-128, 127) as i8)
23931 .collect();
23932 let bias: Vec<i32> = vec![0i32; n];
23933
23934 let _f = DType::F32;
23936 let mut g_q = Graph::new("qmm_direct");
23937 let xn = g_q.input("x", Shape::new(&[m, k], DType::I8));
23938 let wn = g_q.input("w", Shape::new(&[k, n], DType::I8));
23939 let bn = g_q.input("b", Shape::new(&[n], DType::I32));
23940 let out = g_q.q_matmul(xn, wn, bn, 0, 0, 0, mult, Shape::new(&[m, n], DType::I8));
23941 g_q.set_outputs(vec![out]);
23942 let plan = rlx_opt::memory::plan_memory(&g_q);
23943 let mut arena = crate::arena::Arena::from_plan(plan);
23944 let sched = compile_thunks(&g_q, &arena);
23945
23946 let xn_off = arena.byte_offset(xn);
23948 let wn_off = arena.byte_offset(wn);
23949 let bn_off = arena.byte_offset(bn);
23950 let out_off = arena.byte_offset(out);
23951 let buf = arena.raw_buf_mut();
23952 unsafe {
23953 let p = buf.as_mut_ptr().add(xn_off) as *mut i8;
23954 for (i, &v) in xq.iter().enumerate() {
23955 *p.add(i) = v;
23956 }
23957 let p = buf.as_mut_ptr().add(wn_off) as *mut i8;
23958 for (i, &v) in wq.iter().enumerate() {
23959 *p.add(i) = v;
23960 }
23961 let p = buf.as_mut_ptr().add(bn_off) as *mut i32;
23962 for (i, &v) in bias.iter().enumerate() {
23963 *p.add(i) = v;
23964 }
23965 }
23966 execute_thunks(&sched, arena.raw_buf_mut());
23967 let out_q: Vec<i8> = unsafe {
23968 let p = arena.raw_buf().as_ptr().add(out_off) as *const i8;
23969 (0..m * n).map(|i| *p.add(i)).collect()
23970 };
23971
23972 let mut out_ref = vec![0i8; m * n];
23977 for mi in 0..m {
23978 for ni in 0..n {
23979 let mut acc: i32 = 0;
23980 for ki in 0..k {
23981 acc += (xq[mi * k + ki] as i32) * (wq[ki * n + ni] as i32);
23982 }
23983 let r = (acc as f32 * mult).round() as i32;
23984 out_ref[mi * n + ni] = r.clamp(-128, 127) as i8;
23985 }
23986 }
23987
23988 for (i, (a, r)) in out_q.iter().zip(&out_ref).enumerate() {
23989 assert_eq!(a, r, "q_matmul[{i}]: kernel {a} vs reference {r}");
23990 }
23991 }
23992
23993 #[test]
23998 fn quantize_dequantize_round_trip() {
23999 use rlx_ir::Philox4x32;
24000 let len = 64;
24001 let mut rng = Philox4x32::new(2027);
24002 let mut x = vec![0f32; len];
24003 rng.fill_normal(&mut x);
24004 x[0] = 999.0;
24007 x[1] = -999.0;
24008
24009 let scale = 0.05f32;
24010 let zp = 3i32;
24011
24012 let f = DType::F32;
24013 let mut g = Graph::new("qdq");
24014 let xn = g.input("x", Shape::new(&[len], f));
24015 let q = g.quantize(xn, scale, zp);
24016 let dq = g.dequantize(q, scale, zp);
24017 g.set_outputs(vec![dq]);
24018
24019 let plan = rlx_opt::memory::plan_memory(&g);
24020 let mut arena = crate::arena::Arena::from_plan(plan);
24021 let sched = compile_thunks(&g, &arena);
24022 let xn_off = arena.byte_offset(xn);
24023 let dq_off = arena.byte_offset(dq);
24024 let buf = arena.raw_buf_mut();
24025 unsafe {
24026 let p = buf.as_mut_ptr().add(xn_off) as *mut f32;
24027 for (i, &v) in x.iter().enumerate() {
24028 *p.add(i) = v;
24029 }
24030 }
24031 execute_thunks(&sched, arena.raw_buf_mut());
24032 let out: Vec<f32> = unsafe {
24033 let p = arena.raw_buf().as_ptr().add(dq_off) as *const f32;
24034 (0..len).map(|i| *p.add(i)).collect()
24035 };
24036
24037 let sat_pos = (127 - zp) as f32 * scale;
24040 let sat_neg = (-128 - zp) as f32 * scale;
24041 assert!((out[0] - sat_pos).abs() < 1e-6, "+sat: {}", out[0]);
24042 assert!((out[1] - sat_neg).abs() < 1e-6, "-sat: {}", out[1]);
24043
24044 for i in 2..len {
24047 assert!(
24048 (out[i] - x[i]).abs() <= scale + 1e-5,
24049 "qdq[{i}]: {} → {}, scale={scale}",
24050 x[i],
24051 out[i]
24052 );
24053 }
24054 }
24055
24056 #[test]
24062 fn quantize_per_channel_round_trip() {
24063 let c = 4usize;
24064 let inner = 5usize;
24065 let mags = [0.01f32, 0.5, 5.0, 50.0];
24068 let mut x = vec![0f32; c * inner];
24069 for ci in 0..c {
24070 for ii in 0..inner {
24071 x[ci * inner + ii] = match ii {
24075 0 => -mags[ci],
24076 1 => 0.0,
24077 2 => mags[ci],
24078 3 => mags[ci] * 1000.0, _ => -mags[ci] * 1000.0, };
24081 }
24082 }
24083 let scales: Vec<f32> = mags.iter().map(|&m| m / 127.0).collect();
24084 let zps: Vec<i32> = vec![0, 0, 0, 0];
24085
24086 let f = DType::F32;
24087 let mut g = Graph::new("qdq_pc");
24088 let xn = g.input("x", Shape::new(&[c, inner], f));
24089 let q = g.quantize_per_channel(xn, 0, scales.clone(), zps.clone());
24090 let dq = g.dequantize_per_channel(q, 0, scales.clone(), zps);
24091 g.set_outputs(vec![dq]);
24092
24093 let plan = rlx_opt::memory::plan_memory(&g);
24094 let mut arena = crate::arena::Arena::from_plan(plan);
24095 let sched = compile_thunks(&g, &arena);
24096 let xn_off = arena.byte_offset(xn);
24097 let dq_off = arena.byte_offset(dq);
24098 let buf = arena.raw_buf_mut();
24099 unsafe {
24100 let p = buf.as_mut_ptr().add(xn_off) as *mut f32;
24101 for (i, &v) in x.iter().enumerate() {
24102 *p.add(i) = v;
24103 }
24104 }
24105 execute_thunks(&sched, arena.raw_buf_mut());
24106 let out: Vec<f32> = unsafe {
24107 let p = arena.raw_buf().as_ptr().add(dq_off) as *const f32;
24108 (0..c * inner).map(|i| *p.add(i)).collect()
24109 };
24110
24111 for ci in 0..c {
24112 for ii in 0..3 {
24115 let idx = ci * inner + ii;
24116 assert!(
24117 (out[idx] - x[idx]).abs() <= scales[ci] + 1e-5,
24118 "ch {ci} idx {ii}: {} vs {}",
24119 x[idx],
24120 out[idx]
24121 );
24122 }
24123 let sat_pos = 127.0 * scales[ci];
24125 let sat_neg = -128.0 * scales[ci];
24126 assert!(
24127 (out[ci * inner + 3] - sat_pos).abs() < 1e-5,
24128 "ch {ci} +sat: {}",
24129 out[ci * inner + 3]
24130 );
24131 assert!(
24132 (out[ci * inner + 4] - sat_neg).abs() < 1e-5,
24133 "ch {ci} -sat: {}",
24134 out[ci * inner + 4]
24135 );
24136 }
24137 }
24138
24139 #[test]
24145 fn activation_backward_matches_numerical_per_kind() {
24146 use rlx_ir::Philox4x32;
24147 use rlx_ir::op::Activation;
24148 let mut rng = Philox4x32::new(91);
24149 let len = 32;
24150 let mut x_pos = vec![0f32; len];
24155 rng.fill_normal(&mut x_pos);
24156 for v in x_pos.iter_mut() {
24157 *v = v.abs() + 0.5;
24158 }
24159 let mut x_any = vec![0f32; len];
24160 rng.fill_normal(&mut x_any);
24161 let mut dy = vec![0f32; len];
24162 rng.fill_normal(&mut dy);
24163
24164 for &(kind, x_data, eps, tol) in &[
24165 (Activation::Sigmoid, &x_any[..], 1e-3, 5e-3),
24166 (Activation::Tanh, &x_any[..], 1e-3, 5e-3),
24167 (Activation::Silu, &x_any[..], 1e-3, 5e-3),
24168 (Activation::Gelu, &x_any[..], 1e-3, 5e-3),
24169 (Activation::GeluApprox, &x_any[..], 1e-3, 5e-3),
24170 (Activation::Exp, &x_any[..], 1e-4, 5e-3),
24171 (Activation::Log, &x_pos[..], 1e-4, 5e-3),
24172 (Activation::Sqrt, &x_pos[..], 1e-4, 5e-3),
24173 (Activation::Rsqrt, &x_pos[..], 1e-4, 5e-3),
24174 (Activation::Neg, &x_any[..], 1e-3, 5e-4),
24175 ] {
24176 let f = DType::F32;
24177 let mut g = Graph::new("act_bw");
24178 let xn = g.input("x", Shape::new(&[len], f));
24179 let dyn_ = g.input("dy", Shape::new(&[len], f));
24180 let dx = g.activation_backward(kind, xn, dyn_);
24181 g.set_outputs(vec![dx]);
24182
24183 let plan = rlx_opt::memory::plan_memory(&g);
24184 let mut arena = crate::arena::Arena::from_plan(plan);
24185 let sched = compile_thunks(&g, &arena);
24186
24187 let xn_off = arena.byte_offset(xn);
24188 let dyn_off = arena.byte_offset(dyn_);
24189 let dx_off = arena.byte_offset(dx);
24190 let buf = arena.raw_buf_mut();
24191 unsafe {
24192 let p = buf.as_mut_ptr().add(xn_off) as *mut f32;
24193 for (i, &v) in x_data.iter().enumerate() {
24194 *p.add(i) = v;
24195 }
24196 let p = buf.as_mut_ptr().add(dyn_off) as *mut f32;
24197 for (i, &v) in dy.iter().enumerate() {
24198 *p.add(i) = v;
24199 }
24200 }
24201 execute_thunks(&sched, arena.raw_buf_mut());
24202 let analytical: Vec<f32> = unsafe {
24203 let p = arena.raw_buf().as_ptr().add(dx_off) as *const f32;
24204 (0..len).map(|i| *p.add(i)).collect()
24205 };
24206
24207 let act_apply = |kind: Activation, x: f32| -> f32 {
24210 match kind {
24211 Activation::Sigmoid => 1.0 / (1.0 + (-x).exp()),
24212 Activation::Tanh => x.tanh(),
24213 Activation::Silu => x / (1.0 + (-x).exp()),
24214 Activation::Gelu => {
24215 const INV_SQRT2: f32 = 0.707_106_77;
24217 0.5 * x * (1.0 + erf_f32(x * INV_SQRT2))
24218 }
24219 Activation::GeluApprox => {
24220 const C: f32 = 0.797_884_6;
24221 const A: f32 = 0.044_715;
24222 let inner = C * (x + A * x * x * x);
24223 0.5 * x * (1.0 + inner.tanh())
24224 }
24225 Activation::Exp => x.exp(),
24226 Activation::Log => x.ln(),
24227 Activation::Sqrt => x.sqrt(),
24228 Activation::Rsqrt => 1.0 / x.sqrt(),
24229 Activation::Neg => -x,
24230 Activation::Relu => x.max(0.0),
24231 Activation::Abs => x.abs(),
24232 Activation::Round => x.round(),
24233 Activation::Sin => x.sin(),
24234 Activation::Cos => x.cos(),
24235 Activation::Tan => x.tan(),
24236 Activation::Atan => x.atan(),
24237 }
24238 };
24239 for i in 0..len {
24240 let xv = x_data[i];
24241 let plus = act_apply(kind, xv + eps);
24242 let minus = act_apply(kind, xv - eps);
24243 let num = (plus - minus) / (2.0 * eps) * dy[i];
24244 assert!(
24245 (analytical[i] - num).abs() < tol,
24246 "{kind:?}[{i}]: analytical {} vs numerical {num}",
24247 analytical[i]
24248 );
24249 }
24250 }
24251 }
24252
24253 #[test]
24257 fn matmul_3d_gradient_matches_numerical() {
24258 use rlx_ir::Philox4x32;
24259 let batch = 2usize;
24260 let m = 3usize;
24261 let k = 4usize;
24262 let n = 5usize;
24263 let mut rng = Philox4x32::new(101);
24264 let mut a_data = vec![0f32; batch * m * k];
24265 rng.fill_normal(&mut a_data);
24266 let mut b_data = vec![0f32; batch * k * n];
24267 rng.fill_normal(&mut b_data);
24268
24269 let f = DType::F32;
24270 let mut fwd = Graph::new("matmul_3d");
24271 let an = fwd.input("a", Shape::new(&[batch, m, k], f));
24272 let bp = fwd.param("b", Shape::new(&[batch, k, n], f));
24273 let mm = fwd.matmul(an, bp, Shape::new(&[batch, m, n], f));
24274 let loss = fwd.add_node(
24275 Op::Reduce {
24276 op: ReduceOp::Sum,
24277 axes: vec![0, 1, 2],
24278 keep_dim: false,
24279 },
24280 vec![mm],
24281 Shape::from_dims(&[], f),
24282 );
24283 fwd.set_outputs(vec![loss]);
24284
24285 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[bp]);
24286 let d_out = bwd_graph
24287 .nodes()
24288 .iter()
24289 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
24290 .map(|n| n.id)
24291 .unwrap();
24292
24293 let plan = rlx_opt::memory::plan_memory(&bwd_graph);
24294 let mut arena = crate::arena::Arena::from_plan(plan);
24295 let sched = compile_thunks(&bwd_graph, &arena);
24296 for &(id, data) in &[(an, &a_data), (bp, &b_data), (d_out, &vec![1.0f32])] {
24297 let off = arena.byte_offset(id);
24298 let buf = arena.raw_buf_mut();
24299 unsafe {
24300 let p = buf.as_mut_ptr().add(off) as *mut f32;
24301 for (i, &v) in data.iter().enumerate() {
24302 *p.add(i) = v;
24303 }
24304 }
24305 }
24306 execute_thunks(&sched, arena.raw_buf_mut());
24307 let gb_id = bwd_graph.outputs[1];
24308 let g_b: Vec<f32> = unsafe {
24309 let p = arena.raw_buf().as_ptr().add(arena.byte_offset(gb_id)) as *const f32;
24310 (0..batch * k * n).map(|i| *p.add(i)).collect()
24311 };
24312
24313 let forward_loss = |b_vals: &[f32]| -> f32 {
24315 let mut out = vec![0f32; batch * m * n];
24316 for bi in 0..batch {
24317 for mi in 0..m {
24318 for ni in 0..n {
24319 let mut acc = 0f32;
24320 for ki in 0..k {
24321 acc +=
24322 a_data[bi * m * k + mi * k + ki] * b_vals[bi * k * n + ki * n + ni];
24323 }
24324 out[bi * m * n + mi * n + ni] = acc;
24325 }
24326 }
24327 }
24328 out.iter().sum()
24329 };
24330 let eps = 1e-3f32;
24331 let mut bp_p = b_data.clone();
24332 let mut g_b_num = vec![0f32; b_data.len()];
24333 for i in 0..b_data.len() {
24334 let s = bp_p[i];
24335 bp_p[i] = s + eps;
24336 let lp = forward_loss(&bp_p);
24337 bp_p[i] = s - eps;
24338 let lm = forward_loss(&bp_p);
24339 bp_p[i] = s;
24340 g_b_num[i] = (lp - lm) / (2.0 * eps);
24341 }
24342 for (i, (a, n)) in g_b.iter().zip(&g_b_num).enumerate() {
24343 assert!(
24344 (a - n).abs() < 5e-3,
24345 "matmul_3d g_b[{i}]: analytical {a} vs numerical {n}"
24346 );
24347 }
24348 }
24349
24350 #[test]
24356 fn softmax_gradient_matches_numerical() {
24357 use rlx_ir::Philox4x32;
24358 let n = 3usize;
24359 let c = 5usize;
24360 let mut rng = Philox4x32::new(57);
24361 let mut x_data = vec![0f32; n * c];
24362 rng.fill_normal(&mut x_data);
24363
24364 let f = DType::F32;
24365 let mut fwd = Graph::new("softmax_only");
24366 let xn = fwd.input("x", Shape::new(&[n, c], f));
24367 let sm = fwd.add_node(Op::Softmax { axis: -1 }, vec![xn], Shape::new(&[n, c], f));
24368 let loss = fwd.add_node(
24372 Op::Reduce {
24373 op: ReduceOp::Sum,
24374 axes: vec![0, 1],
24375 keep_dim: false,
24376 },
24377 vec![sm],
24378 Shape::from_dims(&[], f),
24379 );
24380 fwd.set_outputs(vec![loss]);
24381
24382 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[xn]);
24386 let d_out = bwd_graph
24387 .nodes()
24388 .iter()
24389 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
24390 .map(|n| n.id)
24391 .unwrap();
24392
24393 let plan = rlx_opt::memory::plan_memory(&bwd_graph);
24394 let mut arena = crate::arena::Arena::from_plan(plan);
24395 let sched = compile_thunks(&bwd_graph, &arena);
24396 for &(id, data) in &[(xn, &x_data), (d_out, &vec![1.0f32])] {
24397 let off = arena.byte_offset(id);
24398 let buf = arena.raw_buf_mut();
24399 unsafe {
24400 let p = buf.as_mut_ptr().add(off) as *mut f32;
24401 for (i, &v) in data.iter().enumerate() {
24402 *p.add(i) = v;
24403 }
24404 }
24405 }
24406 execute_thunks(&sched, arena.raw_buf_mut());
24407 let g_x_id = bwd_graph.outputs[1];
24408 let g_x: Vec<f32> = unsafe {
24409 let p = arena.raw_buf().as_ptr().add(arena.byte_offset(g_x_id)) as *const f32;
24410 (0..n * c).map(|i| *p.add(i)).collect()
24411 };
24412
24413 let forward_loss = |x: &[f32]| -> f32 {
24417 let mut total = 0f32;
24418 for ni in 0..n {
24419 let row = &x[ni * c..(ni + 1) * c];
24420 let m = row.iter().fold(f32::NEG_INFINITY, |a, &v| a.max(v));
24421 let denom: f32 = row.iter().map(|&v| (v - m).exp()).sum();
24422 for &v in row {
24423 total += (v - m).exp() / denom;
24424 }
24425 }
24426 total
24427 };
24428 let eps = 1e-3f32;
24429 let mut p = x_data.clone();
24430 for i in 0..x_data.len() {
24431 let s = p[i];
24432 p[i] = s + eps;
24433 let lp = forward_loss(&p);
24434 p[i] = s - eps;
24435 let lm = forward_loss(&p);
24436 p[i] = s;
24437 let num = (lp - lm) / (2.0 * eps);
24438 assert!(
24439 (g_x[i] - num).abs() < 5e-3,
24440 "softmax g_x[{i}]: analytical {} vs numerical {num}",
24441 g_x[i]
24442 );
24443 }
24444 }
24445
24446 #[test]
24451 fn layer_norm_gradient_matches_numerical() {
24452 use rlx_ir::Philox4x32;
24453 let rows = 3usize;
24454 let h = 6usize;
24455 let mut rng = Philox4x32::new(1009);
24456 let mut x_data = vec![0f32; rows * h];
24457 rng.fill_normal(&mut x_data);
24458 let mut g_data = vec![0f32; h];
24459 rng.fill_normal(&mut g_data);
24460 for v in g_data.iter_mut() {
24461 *v = v.abs() + 0.5;
24462 }
24463 let mut b_data = vec![0f32; h];
24464 rng.fill_normal(&mut b_data);
24465 let eps = 1e-5f32;
24466
24467 let f = DType::F32;
24468 let mut fwd = Graph::new("ln_only");
24469 let xn = fwd.input("x", Shape::new(&[rows, h], f));
24470 let gp = fwd.param("gamma", Shape::new(&[h], f));
24471 let bp = fwd.param("beta", Shape::new(&[h], f));
24472 let ln = fwd.add_node(
24473 Op::LayerNorm { axis: -1, eps },
24474 vec![xn, gp, bp],
24475 Shape::new(&[rows, h], f),
24476 );
24477 let loss = fwd.add_node(
24478 Op::Reduce {
24479 op: ReduceOp::Sum,
24480 axes: vec![0, 1],
24481 keep_dim: false,
24482 },
24483 vec![ln],
24484 Shape::from_dims(&[], f),
24485 );
24486 fwd.set_outputs(vec![loss]);
24487
24488 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[xn, gp, bp]);
24489 let d_out = bwd_graph
24490 .nodes()
24491 .iter()
24492 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
24493 .map(|n| n.id)
24494 .unwrap();
24495
24496 let plan = rlx_opt::memory::plan_memory(&bwd_graph);
24497 let mut arena = crate::arena::Arena::from_plan(plan);
24498 let sched = compile_thunks(&bwd_graph, &arena);
24499 for &(id, data) in &[
24500 (xn, &x_data),
24501 (gp, &g_data),
24502 (bp, &b_data),
24503 (d_out, &vec![1.0f32]),
24504 ] {
24505 let off = arena.byte_offset(id);
24506 let buf = arena.raw_buf_mut();
24507 unsafe {
24508 let p = buf.as_mut_ptr().add(off) as *mut f32;
24509 for (i, &v) in data.iter().enumerate() {
24510 *p.add(i) = v;
24511 }
24512 }
24513 }
24514 execute_thunks(&sched, arena.raw_buf_mut());
24515 let read = |id: NodeId, n: usize| -> Vec<f32> {
24516 let off = arena.byte_offset(id);
24517 unsafe {
24518 let p = arena.raw_buf().as_ptr().add(off) as *const f32;
24519 (0..n).map(|i| *p.add(i)).collect()
24520 }
24521 };
24522 let dx_a = read(bwd_graph.outputs[1], rows * h);
24523 let dg_a = read(bwd_graph.outputs[2], h);
24524 let db_a = read(bwd_graph.outputs[3], h);
24525
24526 let forward_loss = |x: &[f32], g: &[f32], b: &[f32]| -> f32 {
24527 let mut total = 0f32;
24528 for r in 0..rows {
24529 let row = &x[r * h..(r + 1) * h];
24530 let mean = row.iter().sum::<f32>() / h as f32;
24531 let var = row.iter().map(|&v| (v - mean) * (v - mean)).sum::<f32>() / h as f32;
24532 let inv_std = 1.0 / (var + eps).sqrt();
24533 for d in 0..h {
24534 total += ((row[d] - mean) * inv_std) * g[d] + b[d];
24535 }
24536 }
24537 total
24538 };
24539 let h_eps = 1e-3f32;
24540
24541 let mut x_p = x_data.clone();
24542 for i in 0..x_p.len() {
24543 let s = x_p[i];
24544 x_p[i] = s + h_eps;
24545 let lp = forward_loss(&x_p, &g_data, &b_data);
24546 x_p[i] = s - h_eps;
24547 let lm = forward_loss(&x_p, &g_data, &b_data);
24548 x_p[i] = s;
24549 let num = (lp - lm) / (2.0 * h_eps);
24550 assert!(
24551 (dx_a[i] - num).abs() < 5e-3,
24552 "ln dx[{i}]: analytical {} vs numerical {num}",
24553 dx_a[i]
24554 );
24555 }
24556 let mut g_p = g_data.clone();
24557 for i in 0..g_p.len() {
24558 let s = g_p[i];
24559 g_p[i] = s + h_eps;
24560 let lp = forward_loss(&x_data, &g_p, &b_data);
24561 g_p[i] = s - h_eps;
24562 let lm = forward_loss(&x_data, &g_p, &b_data);
24563 g_p[i] = s;
24564 let num = (lp - lm) / (2.0 * h_eps);
24565 assert!(
24566 (dg_a[i] - num).abs() < 5e-3,
24567 "ln dg[{i}]: analytical {} vs numerical {num}",
24568 dg_a[i]
24569 );
24570 }
24571 let mut b_p = b_data.clone();
24572 for i in 0..b_p.len() {
24573 let s = b_p[i];
24574 b_p[i] = s + h_eps;
24575 let lp = forward_loss(&x_data, &g_data, &b_p);
24576 b_p[i] = s - h_eps;
24577 let lm = forward_loss(&x_data, &g_data, &b_p);
24578 b_p[i] = s;
24579 let num = (lp - lm) / (2.0 * h_eps);
24580 assert!(
24581 (db_a[i] - num).abs() < 5e-3,
24582 "ln db[{i}]: analytical {} vs numerical {num}",
24583 db_a[i]
24584 );
24585 }
24586 }
24587
24588 #[test]
24593 fn dense_sce_mean_gradient_matches_numerical() {
24594 use rlx_ir::Philox4x32;
24595 let bs = 4usize;
24596 let k_in = 3usize;
24597 let c = 5usize;
24598 let mut rng = Philox4x32::new(7);
24599 let mut x = vec![0f32; bs * k_in];
24600 rng.fill_normal(&mut x);
24601 let mut w_init = vec![0f32; k_in * c];
24602 rng.fill_normal(&mut w_init);
24603 let mut b_init = vec![0f32; c];
24604 rng.fill_normal(&mut b_init);
24605 let labels: Vec<f32> = (0..bs).map(|i| (i % c) as f32).collect();
24606
24607 let f = DType::F32;
24609 let mut fwd = Graph::new("dense_sce");
24610 let xn = fwd.input("x", Shape::new(&[bs, k_in], f));
24611 let lb = fwd.input("labels", Shape::new(&[bs], f));
24612 let wp = fwd.param("w", Shape::new(&[k_in, c], f));
24613 let bp = fwd.param("b", Shape::new(&[c], f));
24614 let mm = fwd.matmul(xn, wp, Shape::new(&[bs, c], f));
24615 let logits = fwd.binary(BinaryOp::Add, mm, bp, Shape::new(&[bs, c], f));
24616 let loss_per = fwd.softmax_cross_entropy_with_logits(logits, lb);
24617 let loss = fwd.add_node(
24618 Op::Reduce {
24619 op: ReduceOp::Sum,
24620 axes: vec![0],
24621 keep_dim: false,
24622 },
24623 vec![loss_per],
24624 Shape::from_dims(&[], f),
24626 );
24627 fwd.set_outputs(vec![loss]);
24635
24636 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[wp, bp]);
24638 let d_out = bwd_graph
24641 .nodes()
24642 .iter()
24643 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
24644 .map(|n| n.id)
24645 .expect("d_output input");
24646
24647 let (sched, mut arena) = prepare(
24648 &bwd_graph,
24649 &[
24650 (xn, &x),
24651 (lb, &labels),
24652 (wp, &w_init),
24653 (bp, &b_init),
24654 (d_out, &[1.0]),
24655 ],
24656 );
24657 execute_thunks(&sched, arena.raw_buf_mut());
24658
24659 let outs = &bwd_graph.outputs;
24660 let loss_id = outs[0];
24661 let gw_id = outs[1];
24662 let gb_id = outs[2];
24663 let loss_actual = read_arena(&arena, loss_id, 1)[0];
24664 let gw_actual = read_arena(&arena, gw_id, k_in * c);
24665 let gb_actual = read_arena(&arena, gb_id, c);
24666
24667 let plan = rlx_opt::memory::plan_memory(&fwd);
24671 let mut fwd_arena = crate::arena::Arena::from_plan(plan);
24672 let fwd_sched = compile_thunks(&fwd, &fwd_arena);
24673 write_arena(&mut fwd_arena, xn, &x);
24674 write_arena(&mut fwd_arena, lb, &labels);
24675
24676 let run_loss = |arena: &mut crate::arena::Arena, w: &[f32], b: &[f32]| -> f32 {
24677 write_arena(arena, wp, w);
24678 write_arena(arena, bp, b);
24679 execute_thunks(&fwd_sched, arena.raw_buf_mut());
24680 read_arena(arena, loss, 1)[0]
24681 };
24682
24683 let loss_check = run_loss(&mut fwd_arena, &w_init, &b_init);
24686 assert!(
24687 (loss_actual - loss_check).abs() < 1e-4,
24688 "loss mismatch: bwd graph {loss_actual} vs fwd-only {loss_check}"
24689 );
24690
24691 let eps = 1e-3f32;
24692 let mut w_perturbed = w_init.clone();
24693 let mut gw_numerical = vec![0f32; w_init.len()];
24694 for i in 0..w_init.len() {
24695 let saved = w_perturbed[i];
24696 w_perturbed[i] = saved + eps;
24697 let lp = run_loss(&mut fwd_arena, &w_perturbed, &b_init);
24698 w_perturbed[i] = saved - eps;
24699 let lm = run_loss(&mut fwd_arena, &w_perturbed, &b_init);
24700 w_perturbed[i] = saved;
24701 gw_numerical[i] = (lp - lm) / (2.0 * eps);
24702 }
24703 for (i, (a, n)) in gw_actual.iter().zip(&gw_numerical).enumerate() {
24704 assert!(
24705 (a - n).abs() < 5e-3,
24706 "grad_w[{i}]: analytical {a} vs numerical {n}"
24707 );
24708 }
24709
24710 let mut b_perturbed = b_init.clone();
24711 let mut gb_numerical = vec![0f32; b_init.len()];
24712 for i in 0..b_init.len() {
24713 let saved = b_perturbed[i];
24714 b_perturbed[i] = saved + eps;
24715 let lp = run_loss(&mut fwd_arena, &w_init, &b_perturbed);
24716 b_perturbed[i] = saved - eps;
24717 let lm = run_loss(&mut fwd_arena, &w_init, &b_perturbed);
24718 b_perturbed[i] = saved;
24719 gb_numerical[i] = (lp - lm) / (2.0 * eps);
24720 }
24721 for (i, (a, n)) in gb_actual.iter().zip(&gb_numerical).enumerate() {
24722 assert!(
24723 (a - n).abs() < 5e-3,
24724 "grad_b[{i}]: analytical {a} vs numerical {n}"
24725 );
24726 }
24727 }
24728
24729 #[test]
24732 fn dense_sce_mean_reduce_gradient_matches_numerical() {
24733 use rlx_ir::Philox4x32;
24734 let bs = 3usize;
24735 let k_in = 2usize;
24736 let c = 4usize;
24737 let mut rng = Philox4x32::new(13);
24738 let mut x = vec![0f32; bs * k_in];
24739 rng.fill_normal(&mut x);
24740 let mut w_init = vec![0f32; k_in * c];
24741 rng.fill_normal(&mut w_init);
24742 let labels: Vec<f32> = (0..bs).map(|i| (i % c) as f32).collect();
24743
24744 let f = DType::F32;
24745 let mut fwd = Graph::new("dense_sce_mean");
24746 let xn = fwd.input("x", Shape::new(&[bs, k_in], f));
24747 let lb = fwd.input("labels", Shape::new(&[bs], f));
24748 let wp = fwd.param("w", Shape::new(&[k_in, c], f));
24749 let mm = fwd.matmul(xn, wp, Shape::new(&[bs, c], f));
24750 let loss_per = fwd.softmax_cross_entropy_with_logits(mm, lb);
24751 let loss = fwd.add_node(
24752 Op::Reduce {
24753 op: ReduceOp::Mean,
24754 axes: vec![0],
24755 keep_dim: false,
24756 },
24757 vec![loss_per],
24758 Shape::from_dims(&[], f),
24759 );
24760 fwd.set_outputs(vec![loss]);
24761
24762 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[wp]);
24763 let d_out = bwd_graph
24764 .nodes()
24765 .iter()
24766 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
24767 .map(|n| n.id)
24768 .unwrap();
24769
24770 let (sched, mut arena) = prepare(
24771 &bwd_graph,
24772 &[(xn, &x), (lb, &labels), (wp, &w_init), (d_out, &[1.0])],
24773 );
24774 execute_thunks(&sched, arena.raw_buf_mut());
24775
24776 let outs = &bwd_graph.outputs;
24777 let loss_id = outs[0];
24778 let gw_id = outs[1];
24779 let _ = read_arena(&arena, loss_id, 1)[0];
24780 let gw_actual = read_arena(&arena, gw_id, k_in * c);
24781
24782 let plan = rlx_opt::memory::plan_memory(&fwd);
24783 let mut fwd_arena = crate::arena::Arena::from_plan(plan);
24784 let fwd_sched = compile_thunks(&fwd, &fwd_arena);
24785 write_arena(&mut fwd_arena, xn, &x);
24786 write_arena(&mut fwd_arena, lb, &labels);
24787
24788 let run_loss = |arena: &mut crate::arena::Arena, w: &[f32]| -> f32 {
24789 write_arena(arena, wp, w);
24790 execute_thunks(&fwd_sched, arena.raw_buf_mut());
24791 read_arena(arena, loss, 1)[0]
24792 };
24793
24794 let eps = 1e-3f32;
24795 let mut wp_p = w_init.clone();
24796 let mut gw_num = vec![0f32; w_init.len()];
24797 for i in 0..w_init.len() {
24798 let s = wp_p[i];
24799 wp_p[i] = s + eps;
24800 let lp = run_loss(&mut fwd_arena, &wp_p);
24801 wp_p[i] = s - eps;
24802 let lm = run_loss(&mut fwd_arena, &wp_p);
24803 wp_p[i] = s;
24804 gw_num[i] = (lp - lm) / (2.0 * eps);
24805 }
24806 for (i, (a, n)) in gw_actual.iter().zip(&gw_num).enumerate() {
24807 assert!((a - n).abs() < 5e-3, "mean reduce grad_w[{i}]: {a} vs {n}");
24808 }
24809 }
24810 #[test]
24815 fn tinyconv_full_gradient_matches_numerical() {
24816 use rlx_ir::Philox4x32;
24817 let n = 1usize;
24819 let c_in = 1usize;
24820 let h = 6usize;
24821 let w_in = 6usize;
24822 let c_mid = 2usize; let kh = 3;
24824 let kw = 3;
24825 let h1 = h - kh + 1; let w1 = w_in - kw + 1; let h2 = h1 / 2;
24828 let w2 = w1 / 2; let flat = c_mid * h2 * w2; let num_classes = 3usize;
24831
24832 let mut rng = Philox4x32::new(31);
24833 let mut x = vec![0f32; n * c_in * h * w_in];
24834 rng.fill_normal(&mut x);
24835 let mut wc = vec![0f32; c_mid * c_in * kh * kw];
24836 rng.fill_normal(&mut wc);
24837 for v in wc.iter_mut() {
24838 *v *= 0.2;
24839 }
24840 let bc: Vec<f32> = (0..c_mid).map(|i| 5.0 + 0.1 * i as f32).collect();
24849 let mut wfc = vec![0f32; flat * num_classes];
24850 rng.fill_normal(&mut wfc);
24851 for v in wfc.iter_mut() {
24852 *v *= 0.5;
24853 }
24854 let mut bfc = vec![0f32; num_classes];
24855 rng.fill_normal(&mut bfc);
24856 let labels: Vec<f32> = vec![1.0]; let f = DType::F32;
24859 let mut fwd = Graph::new("tinyconv");
24860 let xn = fwd.input("x", Shape::new(&[n, c_in, h, w_in], f));
24861 let lb = fwd.input("labels", Shape::new(&[n], f));
24862 let wcp = fwd.param("wc", Shape::new(&[c_mid, c_in, kh, kw], f));
24863 let bcp = fwd.param("bc", Shape::new(&[c_mid], f));
24864 let wfp = fwd.param("wfc", Shape::new(&[flat, num_classes], f));
24865 let bfp = fwd.param("bfc", Shape::new(&[num_classes], f));
24866
24867 let conv = fwd.add_node(
24869 Op::Conv {
24870 kernel_size: vec![kh, kw],
24871 stride: vec![1, 1],
24872 padding: vec![0, 0],
24873 dilation: vec![1, 1],
24874 groups: 1,
24875 },
24876 vec![xn, wcp],
24877 Shape::new(&[n, c_mid, h1, w1], f),
24878 );
24879 let bc_4d = fwd.add_node(
24891 Op::Reshape {
24892 new_shape: vec![1, c_mid as i64, 1, 1],
24893 },
24894 vec![bcp],
24895 Shape::new(&[1, c_mid, 1, 1], f),
24896 );
24897 let bc_expanded = fwd.add_node(
24898 Op::Expand {
24899 target_shape: vec![n as i64, c_mid as i64, h1 as i64, w1 as i64],
24900 },
24901 vec![bc_4d],
24902 Shape::new(&[n, c_mid, h1, w1], f),
24903 );
24904 let conv_b = fwd.binary(
24905 BinaryOp::Add,
24906 conv,
24907 bc_expanded,
24908 Shape::new(&[n, c_mid, h1, w1], f),
24909 );
24910 let relu = fwd.activation(Activation::Relu, conv_b, Shape::new(&[n, c_mid, h1, w1], f));
24911 let pool = fwd.add_node(
24912 Op::Pool {
24913 kind: ReduceOp::Max,
24914 kernel_size: vec![2, 2],
24915 stride: vec![2, 2],
24916 padding: vec![0, 0],
24917 },
24918 vec![relu],
24919 Shape::new(&[n, c_mid, h2, w2], f),
24920 );
24921 let flatn = fwd.add_node(
24922 Op::Reshape {
24923 new_shape: vec![n as i64, flat as i64],
24924 },
24925 vec![pool],
24926 Shape::new(&[n, flat], f),
24927 );
24928 let mm = fwd.matmul(flatn, wfp, Shape::new(&[n, num_classes], f));
24929 let logits = fwd.binary(BinaryOp::Add, mm, bfp, Shape::new(&[n, num_classes], f));
24930 let loss_per = fwd.softmax_cross_entropy_with_logits(logits, lb);
24931 let loss = fwd.add_node(
24932 Op::Reduce {
24933 op: ReduceOp::Mean,
24934 axes: vec![0],
24935 keep_dim: false,
24936 },
24937 vec![loss_per],
24938 Shape::from_dims(&[], f),
24939 );
24940 fwd.set_outputs(vec![loss]);
24941
24942 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[wcp, bcp, wfp, bfp]);
24943 let d_out = bwd_graph
24944 .nodes()
24945 .iter()
24946 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
24947 .map(|n| n.id)
24948 .unwrap();
24949
24950 let (sched, mut arena) = prepare(
24951 &bwd_graph,
24952 &[
24953 (xn, &x),
24954 (lb, &labels),
24955 (wcp, &wc),
24956 (bcp, &bc),
24957 (wfp, &wfc),
24958 (bfp, &bfc),
24959 (d_out, &[1.0]),
24960 ],
24961 );
24962 execute_thunks(&sched, arena.raw_buf_mut());
24963
24964 let outs = bwd_graph.outputs.clone();
24965 let loss_id = outs[0];
24966 let g_wc_id = outs[1];
24967 let g_bc_id = outs[2];
24968 let g_wfc_id = outs[3];
24969 let g_bfc_id = outs[4];
24970 let loss_actual = read_arena(&arena, loss_id, 1)[0];
24971 let g_wc = read_arena(&arena, g_wc_id, wc.len());
24972 let g_bc = read_arena(&arena, g_bc_id, bc.len());
24973 let g_wfc = read_arena(&arena, g_wfc_id, wfc.len());
24974 let g_bfc = read_arena(&arena, g_bfc_id, bfc.len());
24975
24976 let plan = rlx_opt::memory::plan_memory(&fwd);
24978 let mut fwd_arena = crate::arena::Arena::from_plan(plan);
24979 let fwd_sched = compile_thunks(&fwd, &fwd_arena);
24980 write_arena(&mut fwd_arena, xn, &x);
24981 write_arena(&mut fwd_arena, lb, &labels);
24982
24983 let run_loss = |arena: &mut crate::arena::Arena,
24986 wc: &[f32],
24987 bc: &[f32],
24988 wfc: &[f32],
24989 bfc: &[f32]|
24990 -> f32 {
24991 write_arena(arena, wcp, wc);
24992 write_arena(arena, bcp, bc);
24993 write_arena(arena, wfp, wfc);
24994 write_arena(arena, bfp, bfc);
24995 execute_thunks(&fwd_sched, arena.raw_buf_mut());
24996 read_arena(arena, loss, 1)[0]
24997 };
24998
24999 let loss_check = run_loss(&mut fwd_arena, &wc, &bc, &wfc, &bfc);
25000 assert!(
25001 (loss_actual - loss_check).abs() < 1e-4,
25002 "tinyconv loss mismatch: bwd {loss_actual} vs fwd {loss_check}"
25003 );
25004
25005 let eps = 1e-3f32;
25006 let check_grad = |arena: &mut crate::arena::Arena,
25007 name: &str,
25008 analytical: &[f32],
25009 mut perturb: Box<
25010 dyn FnMut(&mut [f32], usize, f32, &mut crate::arena::Arena) -> f32 + '_,
25011 >,
25012 n: usize| {
25013 for i in 0..n {
25014 let lp = perturb(&mut analytical.to_vec(), i, eps, arena);
25015 let lm = perturb(&mut analytical.to_vec(), i, -eps, arena);
25016 let num = (lp - lm) / (2.0 * eps);
25017 assert!(
25018 (analytical[i] - num).abs() < 5e-3,
25019 "{name}[{i}]: analytical {} vs numerical {num}",
25020 analytical[i]
25021 );
25022 }
25023 };
25024
25025 #[allow(unused_macros)]
25028 macro_rules! sweep {
25029 ($name:expr, $base:expr, $analytical:expr, $set_param:ident) => {{
25030 let n = $base.len();
25031 for i in 0..n {
25032 let mut p = $base.clone();
25033 let s = p[i];
25034 p[i] = s + eps;
25035 let lp = {
25036 let $set_param = &p;
25037 run_loss(&mut fwd_arena, &wc, &bc, &wfc, &bfc).max(f32::NEG_INFINITY);
25038 let _ = $set_param;
25041 0.0_f32
25043 };
25044 let _ = lp;
25045 }
25046 }};
25047 }
25048 let _ = check_grad; for i in 0..wc.len() {
25052 let mut p = wc.clone();
25053 let s = p[i];
25054 p[i] = s + eps;
25055 let lp = run_loss(&mut fwd_arena, &p, &bc, &wfc, &bfc);
25056 p[i] = s - eps;
25057 let lm = run_loss(&mut fwd_arena, &p, &bc, &wfc, &bfc);
25058 let num = (lp - lm) / (2.0 * eps);
25059 assert!(
25060 (g_wc[i] - num).abs() < 5e-3,
25061 "g_wc[{i}]: {} vs {num}",
25062 g_wc[i]
25063 );
25064 }
25065 for i in 0..bc.len() {
25066 let mut p = bc.clone();
25067 let s = p[i];
25068 p[i] = s + eps;
25069 let lp = run_loss(&mut fwd_arena, &wc, &p, &wfc, &bfc);
25070 p[i] = s - eps;
25071 let lm = run_loss(&mut fwd_arena, &wc, &p, &wfc, &bfc);
25072 let num = (lp - lm) / (2.0 * eps);
25073 assert!(
25074 (g_bc[i] - num).abs() < 5e-3,
25075 "g_bc[{i}]: {} vs {num}",
25076 g_bc[i]
25077 );
25078 }
25079 for i in 0..wfc.len() {
25080 let mut p = wfc.clone();
25081 let s = p[i];
25082 p[i] = s + eps;
25083 let lp = run_loss(&mut fwd_arena, &wc, &bc, &p, &bfc);
25084 p[i] = s - eps;
25085 let lm = run_loss(&mut fwd_arena, &wc, &bc, &p, &bfc);
25086 let num = (lp - lm) / (2.0 * eps);
25087 assert!(
25088 (g_wfc[i] - num).abs() < 5e-3,
25089 "g_wfc[{i}]: {} vs {num}",
25090 g_wfc[i]
25091 );
25092 }
25093 for i in 0..bfc.len() {
25094 let mut p = bfc.clone();
25095 let s = p[i];
25096 p[i] = s + eps;
25097 let lp = run_loss(&mut fwd_arena, &wc, &bc, &wfc, &p);
25098 p[i] = s - eps;
25099 let lm = run_loss(&mut fwd_arena, &wc, &bc, &wfc, &p);
25100 let num = (lp - lm) / (2.0 * eps);
25101 assert!(
25102 (g_bfc[i] - num).abs() < 5e-3,
25103 "g_bfc[{i}]: {} vs {num}",
25104 g_bfc[i]
25105 );
25106 }
25107 }
25108
25109 #[test]
25113 fn narrow_rope_skips_when_narrow_has_multiple_consumers() {
25114 let f = DType::F32;
25115 let mut g = Graph::new("nr_skip");
25116 let qkv = g.input("qkv", Shape::new(&[16, 8, 192], f));
25117 let cos = g.input("cos", Shape::new(&[16], f));
25118 let sin = g.input("sin", Shape::new(&[16], f));
25119 let q = g.narrow_(qkv, 2, 0, 64);
25120 let q_rope = g.rope(q, cos, sin, 16);
25121 let q_dup = g.activation(rlx_ir::op::Activation::Relu, q, Shape::new(&[16, 8, 64], f));
25123 g.set_outputs(vec![q_rope, q_dup]);
25124
25125 let plan = rlx_opt::memory::plan_memory(&g);
25126 let arena = crate::arena::Arena::from_plan(plan);
25127 let sched = compile_thunks(&g, &arena);
25128
25129 let narrow_count = sched
25130 .thunks
25131 .iter()
25132 .filter(|t| matches!(t, Thunk::Narrow { .. }))
25133 .count();
25134 assert!(
25135 narrow_count >= 1,
25136 "Narrow with multiple consumers must NOT be fused away"
25137 );
25138 }
25139
25140 #[test]
25153 fn custom_fn_forward_inlines_body() {
25154 let s = Shape::new(&[3], DType::F32);
25155
25156 let mut body = Graph::new("addone_body");
25158 let x = body.input("x", s.clone());
25159 let one_data: Vec<u8> = (0..3).flat_map(|_| 1.0_f32.to_le_bytes()).collect();
25160 let one = body.add_node(Op::Constant { data: one_data }, vec![], s.clone());
25161 let y = body.binary(BinaryOp::Add, x, one, s.clone());
25162 body.set_outputs(vec![y]);
25163
25164 let mut g = Graph::new("custom_fn_outer");
25165 let xin = g.input("x_in", s.clone());
25166 let cf = g.custom_fn(vec![xin], body, None, None);
25167 g.set_outputs(vec![cf]);
25168
25169 let xs = vec![10.0_f32, 20.0, 30.0];
25170 let (sched, mut arena) = prepare(&g, &[(xin, &xs)]);
25171 execute_thunks(&sched, arena.raw_buf_mut());
25172 let got = read_arena(&arena, cf, 3);
25173 assert_eq!(got, vec![11.0, 21.0, 31.0]);
25174 }
25175
25176 fn find_named(graph: &Graph, want: &str) -> NodeId {
25178 for n in graph.nodes() {
25179 let name = match &n.op {
25180 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
25181 _ => None,
25182 };
25183 if name == Some(want) {
25184 return n.id;
25185 }
25186 }
25187 panic!("no node named {want:?} in graph");
25188 }
25189
25190 #[test]
25194 fn custom_fn_vjp_overrides_natural_gradient() {
25195 use rlx_opt::autodiff::grad_with_loss;
25196 let s = Shape::new(&[1], DType::F32);
25197
25198 let mut fwd = Graph::new("id_fwd");
25199 let x = fwd.input("x", s.clone());
25200 fwd.set_outputs(vec![x]);
25201
25202 let mut vjp_g = Graph::new("id_vjp");
25203 let _x_p = vjp_g.input("x", s.clone());
25204 let _y_p = vjp_g.input("primal_output", s.clone());
25205 let dy = vjp_g.input("d_output", s.clone());
25206 let two_data: Vec<u8> = 2.0_f32.to_le_bytes().to_vec();
25207 let two = vjp_g.add_node(Op::Constant { data: two_data }, vec![], s.clone());
25208 let dx = vjp_g.binary(BinaryOp::Mul, dy, two, s.clone());
25209 vjp_g.set_outputs(vec![dx]);
25210
25211 let mut g = Graph::new("outer");
25212 let xp = g.param("x", s.clone());
25213 let cf = g.custom_fn(vec![xp], fwd, Some(vjp_g), None);
25214 g.set_outputs(vec![cf]);
25215
25216 let bwd = grad_with_loss(&g, &[xp]);
25217 assert_eq!(bwd.outputs.len(), 2, "expect [loss, dx]");
25218
25219 let xb = find_named(&bwd, "x");
25220 let dout = find_named(&bwd, "d_output");
25221 let (sched, mut arena) = prepare(&bwd, &[(xb, &[7.0]), (dout, &[1.0])]);
25222 execute_thunks(&sched, arena.raw_buf_mut());
25223 let loss = read_arena(&arena, bwd.outputs[0], 1);
25224 let dx_v = read_arena(&arena, bwd.outputs[1], 1);
25225 assert!((loss[0] - 7.0).abs() < 1e-6, "loss should be 7.0");
25226 assert!(
25227 (dx_v[0] - 2.0).abs() < 1e-6,
25228 "vjp override should yield dx=2.0, got {} (natural autodiff would give 1.0)",
25229 dx_v[0]
25230 );
25231 }
25232
25233 #[test]
25238 fn custom_fn_vjp_two_inputs_matches_mul_autodiff() {
25239 use rlx_opt::autodiff::grad_with_loss;
25240 let s = Shape::new(&[1], DType::F32);
25241
25242 let mut fwd = Graph::new("mul_fwd");
25243 let a_f = fwd.input("a", s.clone());
25244 let b_f = fwd.input("b", s.clone());
25245 let y_f = fwd.binary(BinaryOp::Mul, a_f, b_f, s.clone());
25246 fwd.set_outputs(vec![y_f]);
25247
25248 let mut vjp_g = Graph::new("mul_vjp");
25249 let a_v = vjp_g.input("a", s.clone());
25250 let b_v = vjp_g.input("b", s.clone());
25251 let _y_v = vjp_g.input("primal_output", s.clone());
25252 let dy_v = vjp_g.input("d_output", s.clone());
25253 let da = vjp_g.binary(BinaryOp::Mul, b_v, dy_v, s.clone());
25254 let db = vjp_g.binary(BinaryOp::Mul, a_v, dy_v, s.clone());
25255 vjp_g.set_outputs(vec![da, db]);
25256
25257 let mut g = Graph::new("outer");
25258 let ap = g.param("a", s.clone());
25259 let bp = g.param("b", s.clone());
25260 let cf = g.custom_fn(vec![ap, bp], fwd, Some(vjp_g), None);
25261 g.set_outputs(vec![cf]);
25262
25263 let bwd = grad_with_loss(&g, &[ap, bp]);
25264 assert_eq!(bwd.outputs.len(), 3, "expect [loss, da, db]");
25265
25266 let ab = find_named(&bwd, "a");
25267 let bb = find_named(&bwd, "b");
25268 let dout = find_named(&bwd, "d_output");
25269 let (sched, mut arena) = prepare(&bwd, &[(ab, &[3.0]), (bb, &[5.0]), (dout, &[1.0])]);
25270 execute_thunks(&sched, arena.raw_buf_mut());
25271 let loss = read_arena(&arena, bwd.outputs[0], 1);
25272 let da_v = read_arena(&arena, bwd.outputs[1], 1);
25273 let db_v = read_arena(&arena, bwd.outputs[2], 1);
25274 assert!((loss[0] - 15.0).abs() < 1e-5);
25275 assert!(
25276 (da_v[0] - 5.0).abs() < 1e-5,
25277 "da should be b=5.0, got {}",
25278 da_v[0]
25279 );
25280 assert!(
25281 (db_v[0] - 3.0).abs() < 1e-5,
25282 "db should be a=3.0, got {}",
25283 db_v[0]
25284 );
25285 }
25286
25287 #[test]
25290 fn custom_fn_jvp_overrides_natural_tangent() {
25291 use rlx_opt::autodiff_fwd::jvp;
25292 let s = Shape::new(&[1], DType::F32);
25293
25294 let mut fwd = Graph::new("id_fwd");
25295 let x = fwd.input("x", s.clone());
25296 fwd.set_outputs(vec![x]);
25297
25298 let mut jvp_g = Graph::new("id_jvp");
25299 let _x_p = jvp_g.input("x", s.clone());
25300 let tx = jvp_g.input("tangent_0", s.clone());
25301 let two_data: Vec<u8> = 2.0_f32.to_le_bytes().to_vec();
25302 let two = jvp_g.add_node(Op::Constant { data: two_data }, vec![], s.clone());
25303 let ty = jvp_g.binary(BinaryOp::Mul, tx, two, s.clone());
25304 jvp_g.set_outputs(vec![ty]);
25305
25306 let mut g = Graph::new("outer");
25307 let xin = g.input("x_in", s.clone());
25308 let cf = g.custom_fn(vec![xin], fwd, None, Some(jvp_g));
25309 g.set_outputs(vec![cf]);
25310
25311 let fwd_g = jvp(&g, &[xin]);
25312 assert_eq!(fwd_g.outputs.len(), 2, "expect [primal_y, tangent_y]");
25313
25314 let xb = find_named(&fwd_g, "x_in");
25315 let tan = find_named(&fwd_g, "tangent_x_in");
25316 let (sched, mut arena) = prepare(&fwd_g, &[(xb, &[7.0]), (tan, &[1.0])]);
25317 execute_thunks(&sched, arena.raw_buf_mut());
25318 let y = read_arena(&arena, fwd_g.outputs[0], 1);
25319 let ty_v = read_arena(&arena, fwd_g.outputs[1], 1);
25320 assert!((y[0] - 7.0).abs() < 1e-6);
25321 assert!(
25322 (ty_v[0] - 2.0).abs() < 1e-6,
25323 "jvp override should yield t_y=2.0 (natural autodiff would give 1.0), got {}",
25324 ty_v[0]
25325 );
25326 }
25327
25328 #[test]
25333 fn c64_dtype_storage_layout() {
25334 assert_eq!(
25335 DType::C64.size_bytes(),
25336 8,
25337 "C64 should be 8 bytes (f32 real + f32 imag)"
25338 );
25339 assert!(DType::C64.is_complex());
25340 assert!(!DType::C64.is_float());
25341
25342 let s = Shape::new(&[2], DType::C64);
25344 assert_eq!(s.size_bytes().unwrap(), 16);
25345 }
25346
25347 fn run_c64_binary(op: BinaryOp, a: &[(f32, f32)], b: &[(f32, f32)]) -> Vec<(f32, f32)> {
25354 let n = a.len();
25355 let s = Shape::new(&[n], DType::C64);
25356 let mut g = Graph::new("c64_bin");
25357 let in_a = g.input("a", s.clone());
25358 let in_b = g.input("b", s.clone());
25359 let out = g.binary(op, in_a, in_b, s.clone());
25360 g.set_outputs(vec![out]);
25361
25362 let plan = rlx_opt::memory::plan_memory(&g);
25363 let mut arena = crate::arena::Arena::from_plan(plan);
25364 let sched = compile_thunks(&g, &arena);
25365
25366 let a_off = arena.byte_offset(in_a);
25367 let b_off = arena.byte_offset(in_b);
25368 let out_off = arena.byte_offset(out);
25369 let buf = arena.raw_buf_mut();
25371 unsafe {
25372 let pa = buf.as_mut_ptr().add(a_off) as *mut f32;
25373 let pb = buf.as_mut_ptr().add(b_off) as *mut f32;
25374 for (i, &(re, im)) in a.iter().enumerate() {
25375 *pa.add(2 * i) = re;
25376 *pa.add(2 * i + 1) = im;
25377 }
25378 for (i, &(re, im)) in b.iter().enumerate() {
25379 *pb.add(2 * i) = re;
25380 *pb.add(2 * i + 1) = im;
25381 }
25382 }
25383 execute_thunks(&sched, arena.raw_buf_mut());
25384 let raw_out: Vec<f32> = unsafe {
25385 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
25386 (0..(2 * n)).map(|i| *p.add(i)).collect()
25387 };
25388 (0..n)
25389 .map(|i| (raw_out[2 * i], raw_out[2 * i + 1]))
25390 .collect()
25391 }
25392
25393 #[track_caller]
25394 fn assert_close_c(got: (f32, f32), expected: (f32, f32), tol: f32, label: &str) {
25395 let dr = (got.0 - expected.0).abs();
25396 let di = (got.1 - expected.1).abs();
25397 assert!(
25398 dr < tol && di < tol,
25399 "[{label}] got ({:+.4}, {:+.4}), expected ({:+.4}, {:+.4})",
25400 got.0,
25401 got.1,
25402 expected.0,
25403 expected.1
25404 );
25405 }
25406
25407 #[test]
25408 fn c64_binary_add_matches_complex_arithmetic() {
25409 let a = [(1.0_f32, 2.0_f32), (3.0_f32, -1.0_f32)];
25410 let b = [(4.0_f32, -1.0_f32), (0.5_f32, 0.5_f32)];
25411 let out = run_c64_binary(BinaryOp::Add, &a, &b);
25412 assert_close_c(out[0], (5.0, 1.0), 1e-6, "add[0]");
25413 assert_close_c(out[1], (3.5, -0.5), 1e-6, "add[1]");
25414 }
25415
25416 #[test]
25417 fn c64_binary_sub_matches_complex_arithmetic() {
25418 let a = [(5.0_f32, 1.0_f32)];
25419 let b = [(2.0_f32, 3.0_f32)];
25420 let out = run_c64_binary(BinaryOp::Sub, &a, &b);
25421 assert_close_c(out[0], (3.0, -2.0), 1e-6, "sub");
25422 }
25423
25424 #[test]
25425 fn c64_binary_mul_matches_complex_arithmetic() {
25426 let a = [(1.0_f32, 2.0_f32)];
25428 let b = [(3.0_f32, 4.0_f32)];
25429 let out = run_c64_binary(BinaryOp::Mul, &a, &b);
25430 assert_close_c(out[0], (-5.0, 10.0), 1e-5, "mul");
25431 }
25432
25433 #[test]
25434 fn c64_binary_div_matches_complex_arithmetic() {
25435 let a = [(1.0_f32, 2.0_f32)];
25439 let b = [(3.0_f32, 4.0_f32)];
25440 let out = run_c64_binary(BinaryOp::Div, &a, &b);
25441 assert_close_c(out[0], (0.44, 0.08), 1e-5, "div");
25442 }
25443
25444 #[test]
25445 fn c64_binary_mul_identity_one_is_no_op() {
25446 let a = [(3.5_f32, -1.25_f32), (-2.0_f32, 7.0_f32)];
25448 let b = [(1.0_f32, 0.0_f32), (1.0_f32, 0.0_f32)];
25449 let out = run_c64_binary(BinaryOp::Mul, &a, &b);
25450 assert_close_c(out[0], a[0], 1e-6, "mul·1[0]");
25451 assert_close_c(out[1], a[1], 1e-6, "mul·1[1]");
25452 }
25453
25454 #[test]
25455 fn c64_binary_mul_by_i_rotates_90_degrees() {
25456 let a = [(1.0_f32, 0.0_f32)];
25458 let b = [(0.0_f32, 1.0_f32)];
25459 let out = run_c64_binary(BinaryOp::Mul, &a, &b);
25460 assert_close_c(out[0], (0.0, 1.0), 1e-6, "1·i");
25461 }
25462
25463 #[test]
25464 fn c64_binary_div_by_self_gives_unity() {
25465 let a = [(2.5_f32, -1.5_f32), (-0.7_f32, 4.2_f32)];
25466 let out = run_c64_binary(BinaryOp::Div, &a, &a);
25467 assert_close_c(out[0], (1.0, 0.0), 1e-5, "div_self[0]");
25468 assert_close_c(out[1], (1.0, 0.0), 1e-5, "div_self[1]");
25469 }
25470
25471 #[test]
25472 #[should_panic(expected = "C64: complex max/min/pow")]
25473 fn c64_binary_max_is_rejected_at_lowering() {
25474 run_c64_binary(BinaryOp::Max, &[(1.0_f32, 2.0_f32)], &[(3.0_f32, 4.0_f32)]);
25475 }
25476
25477 fn run_c64_activation(act: Activation, a: &[(f32, f32)]) -> Vec<(f32, f32)> {
25478 let n = a.len();
25479 let s = Shape::new(&[n], DType::C64);
25480 let mut g = Graph::new("c64_act");
25481 let in_a = g.input("a", s.clone());
25482 let out = g.activation(act, in_a, s.clone());
25483 g.set_outputs(vec![out]);
25484 let plan = rlx_opt::memory::plan_memory(&g);
25485 let mut arena = crate::arena::Arena::from_plan(plan);
25486 let sched = compile_thunks(&g, &arena);
25487 let a_off = arena.byte_offset(in_a);
25488 let out_off = arena.byte_offset(out);
25489 let buf = arena.raw_buf_mut();
25490 unsafe {
25491 let pa = buf.as_mut_ptr().add(a_off) as *mut f32;
25492 for (i, &(re, im)) in a.iter().enumerate() {
25493 *pa.add(2 * i) = re;
25494 *pa.add(2 * i + 1) = im;
25495 }
25496 }
25497 execute_thunks(&sched, arena.raw_buf_mut());
25498 let raw: Vec<f32> = unsafe {
25499 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
25500 (0..(2 * n)).map(|i| *p.add(i)).collect()
25501 };
25502 (0..n).map(|i| (raw[2 * i], raw[2 * i + 1])).collect()
25503 }
25504
25505 #[test]
25506 fn c64_activation_neg_negates_both_components() {
25507 let inp = [(3.5_f32, -1.25_f32), (-2.0_f32, 0.0_f32)];
25508 let out = run_c64_activation(Activation::Neg, &inp);
25509 assert_close_c(out[0], (-3.5, 1.25), 1e-6, "neg[0]");
25510 assert_close_c(out[1], (2.0, 0.0), 1e-6, "neg[1]");
25511 }
25512
25513 #[test]
25514 fn c64_activation_exp_matches_euler() {
25515 let inp = [(0.0_f32, std::f32::consts::PI), (1.0_f32, 0.0_f32)];
25518 let out = run_c64_activation(Activation::Exp, &inp);
25519 assert_close_c(out[0], (-1.0, 0.0), 1e-5, "exp(iπ)");
25520 assert_close_c(out[1], (std::f32::consts::E, 0.0), 1e-5, "exp(1)");
25521 }
25522
25523 #[test]
25524 fn c64_activation_log_matches_principal_branch() {
25525 let inp = [(1.0_f32, 0.0_f32), (0.0_f32, 1.0_f32), (-1.0_f32, 0.0_f32)];
25529 let out = run_c64_activation(Activation::Log, &inp);
25530 assert_close_c(out[0], (0.0, 0.0), 1e-5, "log(1)");
25531 assert_close_c(out[1], (0.0, std::f32::consts::FRAC_PI_2), 1e-5, "log(i)");
25532 assert_close_c(out[2], (0.0, std::f32::consts::PI), 1e-5, "log(-1)");
25533 }
25534
25535 #[test]
25536 fn c64_activation_sqrt_squared_recovers_input() {
25537 let inp = [(4.0_f32, 0.0_f32), (3.0_f32, 4.0_f32)];
25540 let roots = run_c64_activation(Activation::Sqrt, &inp);
25541 assert_close_c(roots[0], (2.0, 0.0), 1e-5, "sqrt(4)");
25543 assert_close_c(roots[1], (2.0, 1.0), 1e-5, "sqrt(3+4i)");
25544 }
25545
25546 #[test]
25547 #[should_panic(expected = "no natural complex extension")]
25548 fn c64_activation_relu_is_rejected_at_lowering() {
25549 run_c64_activation(Activation::Relu, &[(1.0_f32, 2.0_f32)]);
25550 }
25551
25552 fn run_complex_norm_sq(z: &[(f32, f32)]) -> Vec<f32> {
25556 let n = z.len();
25557 let mut g = Graph::new("cns_fwd");
25558 let in_z = g.input("z", Shape::new(&[n], DType::C64));
25559 let out = g.complex_norm_sq(in_z);
25560 g.set_outputs(vec![out]);
25561 let plan = rlx_opt::memory::plan_memory(&g);
25562 let mut arena = crate::arena::Arena::from_plan(plan);
25563 let sched = compile_thunks(&g, &arena);
25564 let z_off = arena.byte_offset(in_z);
25565 let out_off = arena.byte_offset(out);
25566 let buf = arena.raw_buf_mut();
25567 unsafe {
25568 let pz = buf.as_mut_ptr().add(z_off) as *mut f32;
25569 for (i, &(re, im)) in z.iter().enumerate() {
25570 *pz.add(2 * i) = re;
25571 *pz.add(2 * i + 1) = im;
25572 }
25573 }
25574 execute_thunks(&sched, arena.raw_buf_mut());
25575 unsafe {
25576 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
25577 (0..n).map(|i| *p.add(i)).collect()
25578 }
25579 }
25580
25581 fn run_complex_norm_sq_bwd(z: &[(f32, f32)], g: &[f32]) -> Vec<(f32, f32)> {
25583 let n = z.len();
25584 let mut gr = Graph::new("cns_bwd");
25585 let in_z = gr.input("z", Shape::new(&[n], DType::C64));
25586 let in_g = gr.input("g", Shape::new(&[n], DType::F32));
25587 let out = gr.complex_norm_sq_backward(in_z, in_g);
25588 gr.set_outputs(vec![out]);
25589 let plan = rlx_opt::memory::plan_memory(&gr);
25590 let mut arena = crate::arena::Arena::from_plan(plan);
25591 let sched = compile_thunks(&gr, &arena);
25592 let z_off = arena.byte_offset(in_z);
25593 let g_off = arena.byte_offset(in_g);
25594 let out_off = arena.byte_offset(out);
25595 let buf = arena.raw_buf_mut();
25596 unsafe {
25597 let pz = buf.as_mut_ptr().add(z_off) as *mut f32;
25598 let pg = buf.as_mut_ptr().add(g_off) as *mut f32;
25599 for (i, &(re, im)) in z.iter().enumerate() {
25600 *pz.add(2 * i) = re;
25601 *pz.add(2 * i + 1) = im;
25602 }
25603 for (i, &v) in g.iter().enumerate() {
25604 *pg.add(i) = v;
25605 }
25606 }
25607 execute_thunks(&sched, arena.raw_buf_mut());
25608 unsafe {
25609 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
25610 (0..n).map(|i| (*p.add(2 * i), *p.add(2 * i + 1))).collect()
25611 }
25612 }
25613
25614 #[test]
25615 fn complex_norm_sq_matches_textbook() {
25616 let z = [(3.0_f32, 4.0_f32), (1.0_f32, 0.0_f32), (0.0_f32, 0.0_f32)];
25620 let out = run_complex_norm_sq(&z);
25621 assert!((out[0] - 25.0).abs() < 1e-5);
25622 assert!((out[1] - 1.0).abs() < 1e-6);
25623 assert!(out[2].abs() < 1e-6);
25624 }
25625
25626 #[test]
25627 fn complex_norm_sq_backward_matches_wirtinger_formula() {
25628 let z = [(3.0_f32, 4.0_f32), (1.5_f32, -2.5_f32)];
25630 let g = [1.0_f32, 1.0_f32];
25631 let dz = run_complex_norm_sq_bwd(&z, &g);
25632 assert_close_c(dz[0], z[0], 1e-6, "dz[0] = g·z[0]");
25633 assert_close_c(dz[1], z[1], 1e-6, "dz[1] = g·z[1]");
25634 }
25635
25636 #[test]
25637 fn complex_norm_sq_backward_scales_with_upstream() {
25638 let z = [(2.0_f32, 1.0_f32), (-1.0_f32, 3.0_f32)];
25640 let g = [0.5_f32, -2.0_f32];
25641 let dz = run_complex_norm_sq_bwd(&z, &g);
25642 assert_close_c(dz[0], (1.0, 0.5), 1e-6, "g=0.5 · (2,1)");
25643 assert_close_c(dz[1], (2.0, -6.0), 1e-6, "g=-2 · (-1,3)");
25644 }
25645
25646 #[test]
25651 fn custom_fn_multi_extracts_each_subgraph_output() {
25652 use rlx_ir::ops::special::MultiOutputHandle;
25653
25654 let _ = MultiOutputHandle {
25655 source: NodeId(0),
25656 sub_shapes: vec![],
25657 offsets: vec![],
25658 }; let mut body = Graph::new("multi_body");
25662 let s3 = Shape::new(&[3], DType::F32);
25663 let x = body.input("x", s3.clone());
25664 let x_sq = body.binary(BinaryOp::Mul, x, x, s3.clone());
25665 let two = body.add_node(
25666 Op::Constant {
25667 data: vec![
25668 2.0_f32.to_le_bytes(),
25669 2.0_f32.to_le_bytes(),
25670 2.0_f32.to_le_bytes(),
25671 ]
25672 .into_iter()
25673 .flatten()
25674 .collect(),
25675 },
25676 vec![],
25677 s3.clone(),
25678 );
25679 let two_x = body.binary(BinaryOp::Mul, two, x, s3.clone());
25680 body.set_outputs(vec![x_sq, two_x]);
25681
25682 let mut outer = Graph::new("multi_outer");
25684 let in_x = outer.input("xin", s3.clone());
25685 let handle = outer.custom_fn_multi(vec![in_x], body);
25686 assert_eq!(handle.n_outputs(), 2);
25687 let out0 = handle.output(&mut outer, 0); let out1 = handle.output(&mut outer, 1); outer.set_outputs(vec![out0, out1]);
25690
25691 let plan = rlx_opt::memory::plan_memory(&outer);
25692 let mut arena = crate::arena::Arena::from_plan(plan);
25693 let sched = compile_thunks(&outer, &arena);
25694 let xin_off = arena.byte_offset(in_x);
25695 let out0_off = arena.byte_offset(out0);
25696 let out1_off = arena.byte_offset(out1);
25697 let xs = [1.0_f32, 2.0, 3.0];
25698 unsafe {
25699 let p = arena.raw_buf_mut().as_mut_ptr().add(xin_off) as *mut f32;
25700 for (i, &v) in xs.iter().enumerate() {
25701 *p.add(i) = v;
25702 }
25703 }
25704 execute_thunks(&sched, arena.raw_buf_mut());
25705 let out0_v: Vec<f32> = unsafe {
25706 let p = arena.raw_buf().as_ptr().add(out0_off) as *const f32;
25707 (0..3).map(|i| *p.add(i)).collect()
25708 };
25709 let out1_v: Vec<f32> = unsafe {
25710 let p = arena.raw_buf().as_ptr().add(out1_off) as *const f32;
25711 (0..3).map(|i| *p.add(i)).collect()
25712 };
25713 for i in 0..3 {
25715 assert!(
25716 (out0_v[i] - xs[i] * xs[i]).abs() < 1e-5,
25717 "out0[{i}] = {} != x² = {}",
25718 out0_v[i],
25719 xs[i] * xs[i]
25720 );
25721 assert!(
25722 (out1_v[i] - 2.0 * xs[i]).abs() < 1e-5,
25723 "out1[{i}] = {} != 2x = {}",
25724 out1_v[i],
25725 2.0 * xs[i]
25726 );
25727 }
25728 }
25729
25730 #[test]
25731 fn complex_norm_sq_gradient_matches_finite_difference() {
25732 let z = [(3.0_f32, 4.0_f32)];
25734 let eps = 1e-3_f32;
25735 let v0 = run_complex_norm_sq(&z)[0];
25736 let z_pert = [(3.0_f32 + eps, 4.0_f32)];
25737 let v1 = run_complex_norm_sq(&z_pert)[0];
25738 let fd_re = (v1 - v0) / eps;
25739 let analytic_re = 2.0 * z[0].0;
25740 assert!((fd_re - analytic_re).abs() < 1e-2);
25741
25742 let z_pert_im = [(3.0_f32, 4.0_f32 + eps)];
25744 let v2 = run_complex_norm_sq(&z_pert_im)[0];
25745 let fd_im = (v2 - v0) / eps;
25746 let analytic_im = 2.0 * z[0].1;
25747 assert!((fd_im - analytic_im).abs() < 1e-2);
25748
25749 let dz = run_complex_norm_sq_bwd(&z, &[1.0_f32]);
25755 assert!((2.0 * dz[0].0 - analytic_re).abs() < 1e-5);
25756 assert!((2.0 * dz[0].1 - analytic_im).abs() < 1e-5);
25757 }
25758
25759 #[test]
25764 fn binary_full_5d_mid_singleton_broadcast() {
25765 let bh = 2usize;
25766 let h = 3;
25767 let w = 4;
25768 let f = DType::F32;
25769
25770 let mut g = Graph::new("bcast_5d");
25771 let lhs = g.input("lhs", Shape::new(&[bh, h, w, h, w], f));
25772 let rhs = g.input("rhs", Shape::new(&[bh, h, w, 1, w], f));
25774 let out = g.binary(BinaryOp::Add, lhs, rhs, Shape::new(&[bh, h, w, h, w], f));
25775 g.set_outputs(vec![out]);
25776
25777 let lhs_data: Vec<f32> = (0..bh * h * w * h * w).map(|i| i as f32 * 0.01).collect();
25779 let rhs_data: Vec<f32> = (0..bh * h * w * w)
25780 .map(|i| (i as f32 + 100.0) * 0.01)
25781 .collect();
25782
25783 let mut expected = vec![0f32; bh * h * w * h * w];
25785 for b_ in 0..bh {
25786 for hq in 0..h {
25787 for wq in 0..w {
25788 for hk in 0..h {
25789 for wk in 0..w {
25790 let li = (((b_ * h + hq) * w + wq) * h + hk) * w + wk;
25791 let ri = ((b_ * h + hq) * w + wq) * w + wk;
25793 expected[li] = lhs_data[li] + rhs_data[ri];
25794 }
25795 }
25796 }
25797 }
25798 }
25799
25800 let plan = rlx_opt::memory::plan_memory(&g);
25801 let mut arena = crate::arena::Arena::from_plan(plan);
25802 let sched = compile_thunks(&g, &arena);
25803 let lhs_off = arena.byte_offset(lhs);
25804 let rhs_off = arena.byte_offset(rhs);
25805 let out_off = arena.byte_offset(out);
25806 let buf = arena.raw_buf_mut();
25807 unsafe {
25808 let p = buf.as_mut_ptr().add(lhs_off) as *mut f32;
25809 for (i, &v) in lhs_data.iter().enumerate() {
25810 *p.add(i) = v;
25811 }
25812 let p = buf.as_mut_ptr().add(rhs_off) as *mut f32;
25813 for (i, &v) in rhs_data.iter().enumerate() {
25814 *p.add(i) = v;
25815 }
25816 }
25817 execute_thunks(&sched, arena.raw_buf_mut());
25818 let actual: Vec<f32> = unsafe {
25819 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
25820 (0..bh * h * w * h * w).map(|i| *p.add(i)).collect()
25821 };
25822
25823 let mut max_diff = 0f32;
25825 let mut max_idx = 0;
25826 for i in 0..actual.len() {
25827 let d = (actual[i] - expected[i]).abs();
25828 if d > max_diff {
25829 max_diff = d;
25830 max_idx = i;
25831 }
25832 }
25833 assert!(
25834 max_diff < 1e-6,
25835 "5D mid-shape singleton broadcast wrong: max |Δ| = {max_diff} at idx {max_idx} \
25836 (actual={}, expected={})",
25837 actual[max_idx],
25838 expected[max_idx]
25839 );
25840 }
25841
25842 #[test]
25843 fn layer_norm2d_and_conv_transpose2d_kernels() {
25844 let mut out = vec![0f32; 8];
25845 crate::kernels::layer_norm2d_nchw(
25846 &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
25847 &[1.0, 1.0],
25848 &[0.0, 0.0],
25849 &mut out,
25850 1,
25851 2,
25852 2,
25853 2,
25854 1e-5,
25855 );
25856 let mean0: f32 = (1.0 + 3.0) / 2.0;
25857 assert!((out[0] - mean0).abs() > 0.1);
25858
25859 let mut up = vec![0f32; 4];
25860 crate::kernels::conv_transpose2d_nchw(
25861 &[2.0],
25862 &[1.0, 0.0, 0.0, 1.0],
25863 &mut up,
25864 1,
25865 1,
25866 1,
25867 1,
25868 1,
25869 2,
25870 2,
25871 2,
25872 2,
25873 2,
25874 2,
25875 0,
25876 0,
25877 1,
25878 1,
25879 1,
25880 );
25881 assert!((up[0] - 2.0).abs() < 1e-5);
25882 assert!((up[3] - 2.0).abs() < 1e-5);
25883 }
25884
25885 #[test]
25891 fn scaled_matmul_oracle_matches_f32() {
25892 use rlx_ir::ScaledFormat::*;
25893 use rlx_ir::{ScaleLayout, ScaledFormat};
25894
25895 fn cosine(a: &[f32], b: &[f32]) -> f32 {
25896 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
25897 let na = a.iter().map(|x| x * x).sum::<f32>().sqrt();
25898 let nb = b.iter().map(|x| x * x).sum::<f32>().sqrt();
25899 dot / (na * nb)
25900 }
25901
25902 #[allow(clippy::too_many_arguments)]
25903 fn run_scaled(
25904 lhs: &[f32],
25905 rhs: &[f32],
25906 m: usize,
25907 k: usize,
25908 n: usize,
25909 lf: ScaledFormat,
25910 rf: ScaledFormat,
25911 layout: ScaleLayout,
25912 ) -> Vec<f32> {
25913 let f = DType::F32;
25914 let u8t = DType::U8;
25915 let mut g = Graph::new("scaled");
25916 let lhs_in = g.input("lhs", Shape::new(&[m, k], f));
25917 let rhs_in = g.input("rhs", Shape::new(&[n, k], f));
25918 let (ls_shape, rs_shape) = match layout {
25919 ScaleLayout::PerTensor => (Shape::new(&[1], f), Shape::new(&[1], f)),
25920 _ => {
25921 let nb = k.div_ceil(layout.block() as usize);
25922 (Shape::new(&[m, nb], u8t), Shape::new(&[n, nb], u8t))
25923 }
25924 };
25925 let ls = g.add_node(
25926 Op::ScaledQuantScale {
25927 format: lf,
25928 scale_layout: layout,
25929 },
25930 vec![lhs_in],
25931 ls_shape,
25932 );
25933 let lq = g.add_node(
25934 Op::ScaledQuantize {
25935 format: lf,
25936 scale_layout: layout,
25937 },
25938 vec![lhs_in, ls],
25939 Shape::new(&[m, k], u8t),
25940 );
25941 let rs = g.add_node(
25942 Op::ScaledQuantScale {
25943 format: rf,
25944 scale_layout: layout,
25945 },
25946 vec![rhs_in],
25947 rs_shape,
25948 );
25949 let rq = g.add_node(
25950 Op::ScaledQuantize {
25951 format: rf,
25952 scale_layout: layout,
25953 },
25954 vec![rhs_in, rs],
25955 Shape::new(&[n, k], u8t),
25956 );
25957 let out = g.add_node(
25958 Op::ScaledMatMul {
25959 lhs_format: lf,
25960 rhs_format: rf,
25961 scale_layout: layout,
25962 has_bias: false,
25963 },
25964 vec![lq, rq, ls, rs],
25965 Shape::new(&[m, n], f),
25966 );
25967 g.set_outputs(vec![out]);
25968
25969 let plan = rlx_opt::memory::plan_memory(&g);
25970 let mut arena = crate::arena::Arena::from_plan(plan);
25971 let sched = compile_thunks(&g, &arena);
25972 let lhs_off = arena.byte_offset(lhs_in);
25973 let rhs_off = arena.byte_offset(rhs_in);
25974 let out_off = arena.byte_offset(out);
25975 let buf = arena.raw_buf_mut();
25976 unsafe {
25977 let lp = buf.as_mut_ptr().add(lhs_off) as *mut f32;
25978 for (i, &v) in lhs.iter().enumerate() {
25979 *lp.add(i) = v;
25980 }
25981 let rp = buf.as_mut_ptr().add(rhs_off) as *mut f32;
25982 for (i, &v) in rhs.iter().enumerate() {
25983 *rp.add(i) = v;
25984 }
25985 }
25986 execute_thunks(&sched, arena.raw_buf_mut());
25987 unsafe {
25988 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
25989 (0..m * n).map(|i| *p.add(i)).collect()
25990 }
25991 }
25992
25993 let (m, k, n) = (4usize, 64usize, 8usize);
25994 let lhs: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.13).sin() * 1.5).collect();
25995 let rhs: Vec<f32> = (0..n * k).map(|i| (i as f32 * 0.07).cos() * 1.2).collect();
25996 let mut reference = vec![0f32; m * n];
25997 for i in 0..m {
25998 for j in 0..n {
25999 let mut acc = 0f32;
26000 for p in 0..k {
26001 acc += lhs[i * k + p] * rhs[j * k + p];
26002 }
26003 reference[i * n + j] = acc;
26004 }
26005 }
26006
26007 let cases = [
26009 (F8E4M3, 0.999f32),
26010 (F8E5M2, 0.99),
26011 (F8E4M3Fnuz, 0.999),
26012 (F8E5M2Fnuz, 0.99),
26013 (F6E2M3, 0.99),
26014 (F6E3M2, 0.98),
26015 (F4E2M1, 0.90),
26016 ];
26017 for (fmt, thresh) in cases {
26018 let out = run_scaled(&lhs, &rhs, m, k, n, fmt, fmt, ScaleLayout::PerTensor);
26019 let c = cosine(&out, &reference);
26020 assert!(c >= thresh, "{fmt} per-tensor cosine {c} < {thresh}");
26021 }
26022
26023 let out_mx = run_scaled(&lhs, &rhs, m, k, n, F8E4M3, F8E4M3, ScaleLayout::mx());
26025 let c_mx = cosine(&out_mx, &reference);
26026 assert!(c_mx >= 0.999, "mx-e8m0 e4m3 cosine {c_mx}");
26027 let out_nv = run_scaled(&lhs, &rhs, m, k, n, F4E2M1, F4E2M1, ScaleLayout::nvfp4());
26028 let c_nv = cosine(&out_nv, &reference);
26029 assert!(c_nv >= 0.95, "nvfp4 e2m1 cosine {c_nv}");
26030 }
26031
26032 #[test]
26037 fn scaled_dequantize_inverts_quantize() {
26038 use rlx_ir::ScaledFormat::*;
26039 use rlx_ir::{ScaleLayout, ScaledFormat};
26040
26041 fn cosine(a: &[f32], b: &[f32]) -> f32 {
26042 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
26043 let na = a.iter().map(|x| x * x).sum::<f32>().sqrt();
26044 let nb = b.iter().map(|x| x * x).sum::<f32>().sqrt();
26045 dot / (na * nb)
26046 }
26047
26048 fn roundtrip(x: &[f32], rows: usize, cols: usize, fmt: ScaledFormat) -> Vec<f32> {
26049 let f = DType::F32;
26050 let u8t = DType::U8;
26051 let layout = ScaleLayout::PerTensor;
26052 let mut g = Graph::new("dequant_rt");
26053 let x_in = g.input("x", Shape::new(&[rows, cols], f));
26054 let scale = g.add_node(
26055 Op::ScaledQuantScale {
26056 format: fmt,
26057 scale_layout: layout,
26058 },
26059 vec![x_in],
26060 Shape::new(&[1], f),
26061 );
26062 let codes = g.add_node(
26063 Op::ScaledQuantize {
26064 format: fmt,
26065 scale_layout: layout,
26066 },
26067 vec![x_in, scale],
26068 Shape::new(&[rows, cols], u8t),
26069 );
26070 let recon = g.add_node(
26071 Op::ScaledDequantize {
26072 format: fmt,
26073 scale_layout: layout,
26074 },
26075 vec![codes, scale],
26076 Shape::new(&[rows, cols], f),
26077 );
26078 g.set_outputs(vec![recon]);
26079
26080 let plan = rlx_opt::memory::plan_memory(&g);
26081 let mut arena = crate::arena::Arena::from_plan(plan);
26082 let sched = compile_thunks(&g, &arena);
26083 let x_off = arena.byte_offset(x_in);
26084 let r_off = arena.byte_offset(recon);
26085 unsafe {
26086 let p = arena.raw_buf_mut().as_mut_ptr().add(x_off) as *mut f32;
26087 for (i, &v) in x.iter().enumerate() {
26088 *p.add(i) = v;
26089 }
26090 }
26091 execute_thunks(&sched, arena.raw_buf_mut());
26092 unsafe {
26093 let p = arena.raw_buf().as_ptr().add(r_off) as *const f32;
26094 (0..rows * cols).map(|i| *p.add(i)).collect()
26095 }
26096 }
26097
26098 let (rows, cols) = (4usize, 16usize);
26099 let x: Vec<f32> = (0..rows * cols)
26100 .map(|i| (i as f32 * 0.21).sin() * 1.7)
26101 .collect();
26102 for (fmt, min_cos) in [(F8E4M3, 0.999f32), (F8E5M2, 0.99), (F4E2M1, 0.93)] {
26104 let recon = roundtrip(&x, rows, cols, fmt);
26105 assert_eq!(recon.len(), x.len());
26106 assert!(
26107 recon.iter().all(|v| v.is_finite()),
26108 "{fmt:?} produced non-finite"
26109 );
26110 let c = cosine(&recon, &x);
26111 assert!(c >= min_cos, "{fmt:?} round-trip cosine {c} < {min_cos}");
26112 }
26113 }
26114
26115 #[test]
26119 fn fma_matches_mul_add() {
26120 let f = DType::F32;
26121 let n = 9usize;
26122 let a: Vec<f32> = vec![1.5, -2.0, 0.0, 3.25, -1.1, 7.0, -0.5, 2.2, 9.9];
26123 let b: Vec<f32> = vec![2.0, 0.5, 4.0, -1.0, 6.0, -2.5, 8.0, -3.3, 0.1];
26124 let c: Vec<f32> = vec![0.25, 1.0, -3.0, 2.0, -0.5, 4.0, 1.5, -2.2, 0.0];
26125
26126 let mut g = Graph::new("fma");
26127 let an = g.input("a", Shape::new(&[n], f));
26128 let bn = g.input("b", Shape::new(&[n], f));
26129 let cn = g.input("c", Shape::new(&[n], f));
26130 let out = g.add_node(Op::Fma, vec![an, bn, cn], Shape::new(&[n], f));
26131 g.set_outputs(vec![out]);
26132
26133 let actual = run_graph(&g, &[(an, &a), (bn, &b), (cn, &c)], out, n);
26134 for i in 0..n {
26135 let expected = a[i].mul_add(b[i], c[i]);
26136 assert!(
26137 (actual[i] - expected).abs() <= f32::EPSILON * (1.0 + expected.abs()),
26138 "fma[{i}]: {} vs mul_add {expected}",
26139 actual[i]
26140 );
26141 }
26142 }
26143
26144 #[test]
26149 fn scaled_quant_pass_runs_end_to_end() {
26150 use rlx_opt::rlx_compile::scaled_quant_insert::{ScaledQuantConfig, insert_scaled_matmul};
26151
26152 let f = DType::F32;
26153 let (m, k, n) = (3usize, 16usize, 5usize);
26154 let mut g = Graph::new("amp_fp8");
26155 let x = g.input("x", Shape::new(&[m, k], f));
26156 let w = g.param("w", Shape::new(&[k, n], f));
26157 let mm = g.matmul(x, w, Shape::new(&[m, n], f));
26158 g.set_outputs(vec![mm]);
26159
26160 let g = insert_scaled_matmul(g, ScaledQuantConfig::fp8_e4m3());
26161
26162 let x_data: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.11).sin()).collect();
26163 let w_data: Vec<f32> = (0..k * n).map(|i| (i as f32 * 0.05).cos()).collect();
26164 let mut reference = vec![0f32; m * n];
26166 for i in 0..m {
26167 for j in 0..n {
26168 let mut acc = 0f32;
26169 for p in 0..k {
26170 acc += x_data[i * k + p] * w_data[p * n + j];
26171 }
26172 reference[i * n + j] = acc;
26173 }
26174 }
26175
26176 let mut x_id = None;
26178 let mut w_id = None;
26179 for node in g.nodes() {
26180 match &node.op {
26181 Op::Input { name } if name == "x" => x_id = Some(node.id),
26182 Op::Param { name } if name == "w" => w_id = Some(node.id),
26183 _ => {}
26184 }
26185 }
26186 let (x_id, w_id) = (x_id.unwrap(), w_id.unwrap());
26187 let out_id = g.outputs[0];
26188
26189 let plan = rlx_opt::memory::plan_memory(&g);
26190 let mut arena = crate::arena::Arena::from_plan(plan);
26191 let sched = compile_thunks(&g, &arena);
26192 let x_off = arena.byte_offset(x_id);
26193 let w_off = arena.byte_offset(w_id);
26194 let out_off = arena.byte_offset(out_id);
26195 let buf = arena.raw_buf_mut();
26196 unsafe {
26197 let xp = buf.as_mut_ptr().add(x_off) as *mut f32;
26198 for (i, &v) in x_data.iter().enumerate() {
26199 *xp.add(i) = v;
26200 }
26201 let wp = buf.as_mut_ptr().add(w_off) as *mut f32;
26202 for (i, &v) in w_data.iter().enumerate() {
26203 *wp.add(i) = v;
26204 }
26205 }
26206 execute_thunks(&sched, arena.raw_buf_mut());
26207 let actual: Vec<f32> = unsafe {
26208 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
26209 (0..m * n).map(|i| *p.add(i)).collect()
26210 };
26211
26212 let dot: f32 = actual.iter().zip(&reference).map(|(a, b)| a * b).sum();
26213 let na = actual.iter().map(|x| x * x).sum::<f32>().sqrt();
26214 let nb = reference.iter().map(|x| x * x).sum::<f32>().sqrt();
26215 let cos = dot / (na * nb);
26216 assert!(cos >= 0.999, "AMP-fp8 e2e cosine {cos}");
26217 }
26218}