1use std::collections::{HashMap, HashSet};
23use std::num::NonZeroU64;
24
25use rlx_ir::dynamic::{bind_graph, infer_bindings_from_f32_inputs, same_binding};
26use rlx_ir::op::{Activation, BinaryOp, CmpOp, ReduceOp};
27use rlx_ir::shape::DimBinding;
28use rlx_ir::{Graph, NodeId, Op};
29
30use crate::buffer::{Arena, ReadbackStaging, TinyReadbackStaging};
31use crate::device::wgpu_device;
32use crate::kernels::{
33 ArgmaxParams, AttentionBwdParams, AttentionParams, BatchElementwiseRegionParams, BinaryParams,
34 Conv1dParams, Conv2dParams, Conv3dParams, CopyParams, CumsumBwdParams, CumsumParams,
35 DequantMatmulParams, ElementwiseRegionParams, ExpandParams, FmaParams, FusedResidualLnParams,
36 FusedResidualLnTeeParams, FusedResidualRmsNormParams, GatherAxisParams, GatherBwdParams,
37 GatherParams, GroupedMatmulParams, GruParams, Im2Col2dParams, Kernel, LayerNormBwdParams,
38 LayerNormParams, Mamba2Params, MatmulQkvParams, NarrowConcatParams, Pool1dParams, Pool2dParams,
39 Pool3dParams, ReduceParams, RmsNormBwdParams, RnnParams, RopeBwdParams, RopeParams,
40 SampleParams, ScatterAddParams, SceParams, SelectiveScanParams, SoftmaxParams, TopKParams,
41 TransposeParams, UmapKnnParams, UnaryParams, WelchPeaksGpuParams, WhereParams,
42 gather_split_kernel, matmul_coop_f16_vulkan_kernel,
43};
44fn compute_scratch_bytes(graph: &rlx_ir::Graph) -> usize {
48 const ROWS_PER_WG: u32 = 16;
49 let mut max_bytes = 0usize;
50 for node in graph.nodes() {
51 if matches!(
57 &node.op,
58 rlx_ir::Op::LayerNorm { .. } | rlx_ir::Op::RmsNorm { .. }
59 ) {
60 let x_shape = &graph.node(node.inputs[0]).shape;
61 let h_dim = x_shape.dim(x_shape.rank() - 1);
62 if h_dim.is_static() {
63 let h = h_dim.unwrap_static();
64 let bytes = ((h * 4).div_ceil(256) * 256) * 2;
66 if bytes > max_bytes {
67 max_bytes = bytes;
68 }
69 }
70 }
71 if let rlx_ir::Op::LayerNormBackwardGamma { .. } = &node.op {
72 let x_shape = &graph.node(node.inputs[0]).shape;
73 let Some(elems) = x_shape.num_elements() else {
74 continue;
75 };
76 let h_dim = x_shape.dim(x_shape.rank() - 1);
77 if !h_dim.is_static() {
78 continue;
79 }
80 let h = h_dim.unwrap_static();
81 if h == 0 {
82 continue;
83 }
84 let rows = (elems / h) as u32;
85 let num_workgroups = rows.div_ceil(ROWS_PER_WG.max(1));
86 let bytes = (num_workgroups as usize) * h * 4;
87 if bytes > max_bytes {
88 max_bytes = bytes;
89 }
90 }
91 }
92 max_bytes.max(64 * 1024 * 1024)
96}
97
98const CONV_IM2COL_MIN_SPATIAL: u64 = 2048;
106const CONV_IM2COL_MIN_K: u64 = 256;
107const CONV_IM2COL_MIN_COUT: u64 = 64;
108
109const CONV_IM2COL_MAX_COL_BYTES: u64 = 1024 * 1024 * 1024;
113
114fn im2col_min_spatial() -> u64 {
115 rlx_ir::env::parse_or("RLX_WGPU_IM2COL_MIN_SPATIAL", CONV_IM2COL_MIN_SPATIAL)
116}
117fn im2col_min_k() -> u64 {
118 rlx_ir::env::parse_or("RLX_WGPU_IM2COL_MIN_K", CONV_IM2COL_MIN_K)
119}
120fn im2col_min_cout() -> u64 {
121 rlx_ir::env::parse_or("RLX_WGPU_IM2COL_MIN_COUT", CONV_IM2COL_MIN_COUT)
122}
123
124const CONV_TILED_MIN_SPATIAL: u64 = 256;
128fn conv_tiled_min_spatial() -> u64 {
129 rlx_ir::env::parse_or("RLX_WGPU_TILED_MIN_SPATIAL", CONV_TILED_MIN_SPATIAL)
130}
131
132fn conv_im2col_col_elems(
138 op: &Op,
139 in_shape: &rlx_ir::Shape,
140 w_shape: &rlx_ir::Shape,
141 out_shape: &rlx_ir::Shape,
142) -> Option<u64> {
143 let Op::Conv {
144 kernel_size,
145 groups,
146 ..
147 } = op
148 else {
149 return None;
150 };
151 if *groups != 1 {
152 return None;
153 }
154 if !(kernel_size.len() == 2
155 && in_shape.rank() == 4
156 && w_shape.rank() == 4
157 && out_shape.rank() == 4)
158 {
159 return None;
160 }
161 for d in [
162 in_shape.dim(0),
163 in_shape.dim(1),
164 in_shape.dim(2),
165 in_shape.dim(3),
166 ] {
167 if !d.is_static() {
168 return None;
169 }
170 }
171 if !out_shape.dim(2).is_static() || !out_shape.dim(3).is_static() {
172 return None;
173 }
174 if !out_shape.dim(1).is_static() {
175 return None;
176 }
177 if in_shape.dim(0).unwrap_static() != 1 {
178 return None;
179 }
180 let c_in = in_shape.dim(1).unwrap_static() as u64;
181 let c_out = out_shape.dim(1).unwrap_static() as u64;
182 let h_in = in_shape.dim(2).unwrap_static() as u32;
183 let w_in = in_shape.dim(3).unwrap_static() as u32;
184 let one_d = h_in == 1
185 && w_in > 1
186 && kernel_size[0] > 1
187 && kernel_size.get(1).copied().unwrap_or(1) == 1;
188 let (kh, kw, spatial) = if one_d {
189 (
190 kernel_size[0] as u64,
191 1u64,
192 out_shape.dim(3).unwrap_static() as u64,
193 )
194 } else {
195 (
196 kernel_size[0] as u64,
197 kernel_size.get(1).copied().unwrap_or(1) as u64,
198 out_shape.dim(2).unwrap_static() as u64 * out_shape.dim(3).unwrap_static() as u64,
199 )
200 };
201 let k_total = c_in * kh * kw;
202 if kh * kw < 2 {
203 return None;
204 }
205 if spatial < im2col_min_spatial() || k_total < im2col_min_k() || c_out < im2col_min_cout() {
206 return None;
207 }
208 Some(k_total * spatial)
209}
210
211fn conv_im2col_scratch_bytes(graph: &Graph, planned_arena_size: usize, max_binding: u64) -> usize {
216 let mut max_col_bytes: u64 = 0;
217 for node in graph.nodes() {
218 if node.inputs.len() < 2 {
219 continue;
220 }
221 let in_shape = &graph.node(node.inputs[0]).shape;
222 let w_shape = &graph.node(node.inputs[1]).shape;
223 let Some(elems) = conv_im2col_col_elems(&node.op, in_shape, w_shape, &node.shape) else {
224 continue;
225 };
226 let col_bytes = elems.saturating_mul(4);
227 if col_bytes > CONV_IM2COL_MAX_COL_BYTES {
228 continue;
229 }
230 if (planned_arena_size as u64).saturating_add(col_bytes) > max_binding {
231 continue;
232 }
233 max_col_bytes = max_col_bytes.max(col_bytes);
234 }
235 (max_col_bytes.div_ceil(256) * 256) as usize
236}
237
238fn hash_f32_input(data: &[f32]) -> u64 {
241 let bytes = bytemuck::cast_slice(data);
242 let mut h: u64 = 0xcbf29ce484222325;
243 h ^= data.len() as u64;
244 h = h.wrapping_mul(0x100000001b3);
245 for chunk in bytes.chunks(8) {
246 let mut arr = [0u8; 8];
247 arr[..chunk.len()].copy_from_slice(chunk);
248 h ^= u64::from_le_bytes(arr);
249 h = h.wrapping_mul(0x100000001b3);
250 }
251 h
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265enum MatmulCompute {
266 F32,
267 F16,
268 Coop16,
269 CoopF32,
274 CoopF16Vk,
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281enum MatmulQkvKind {
282 F32,
283 CoopF32,
284 CoopF16Vk,
285}
286
287#[allow(dead_code)]
297#[derive(Debug, Clone, Copy)]
298struct CastF32ToF16Params {
299 pub src_off: u32, pub len: u32,
301 pub _p0: u32,
302 pub _p1: u32,
303}
304unsafe impl bytemuck::Pod for CastF32ToF16Params {}
305unsafe impl bytemuck::Zeroable for CastF32ToF16Params {}
306
307#[allow(dead_code)]
316enum Step {
317 CastF32ToF16 {
318 params: CastF32ToF16Params,
319 },
320 Matmul {
321 m: u32,
322 k: u32,
323 n: u32,
324 a_off_f32: u32,
325 b_off_f32: u32,
326 c_off_f32: u32,
327 batch: u32,
328 a_batch_stride: u32,
329 b_batch_stride: u32,
330 c_batch_stride: u32,
331 has_bias: u32,
332 bias_off_f32: u32,
333 act_id: u32, b_is_param: bool,
340 compute_precision: MatmulCompute,
346 },
347 Binary {
348 params: BinaryParams,
349 },
350 Compare {
351 params: BinaryParams,
352 },
353 Unary {
354 params: UnaryParams,
355 f16_mirror: bool,
356 },
357 Where {
358 params: WhereParams,
359 },
360 Fma {
361 params: FmaParams,
362 },
363 Reduce {
364 params: ReduceParams,
365 },
366 Softmax {
367 params: SoftmaxParams,
368 },
369 SoftmaxCrossEntropy {
370 params: SceParams,
371 },
372 LayerNorm {
373 params: LayerNormParams,
374 },
375 Cumsum {
376 params: CumsumParams,
377 },
378 FftGpu {
380 src_off: u32,
381 dst_off: u32,
382 outer: u32,
383 n: u32,
384 inverse: u32,
385 norm_scale: f32,
386 },
387 FftHost {
390 src_byte_off: u32,
391 dst_byte_off: u32,
392 outer: u32,
393 n_complex: u32,
394 inverse: bool,
395 norm_tag: u32,
396 dtype_tag: u32,
397 },
398 ScanHost {
401 plan: std::sync::Arc<rlx_cpu::thunk::ScanBodyPlan>,
402 outer_init_off: usize,
403 outer_final_off: usize,
404 length: u32,
405 save_trajectory: bool,
406 xs_outer: Vec<(usize, usize)>,
407 bcast_outer: Vec<(usize, usize)>,
408 },
409 WelchPeaksHost {
411 spec_byte_off: u32,
412 dst_byte_off: u32,
413 welch_batch: u32,
414 n_fft: u32,
415 n_segments: u32,
416 k: u32,
417 },
418 LogMelHost {
419 spec_byte_off: u32,
420 filt_byte_off: u32,
421 dst_byte_off: u32,
422 outer: u32,
423 n_fft: u32,
424 n_bins: u32,
425 n_mels: u32,
426 },
427 LogMelBackwardHost {
428 spec_byte_off: u32,
429 filt_byte_off: u32,
430 dy_byte_off: u32,
431 dst_byte_off: u32,
432 outer: u32,
433 n_fft: u32,
434 n_bins: u32,
435 n_mels: u32,
436 },
437 Im2ColHost {
439 x_byte_off: u32,
440 col_byte_off: u32,
441 n: u32,
442 c_in: u32,
443 h: u32,
444 w: u32,
445 h_out: u32,
446 w_out: u32,
447 kh: u32,
448 kw: u32,
449 sh: u32,
450 sw: u32,
451 ph: u32,
452 pw: u32,
453 dh: u32,
454 dw_dil: u32,
455 },
456 RngNormalHost {
458 dst_byte_off: u32,
459 len: u32,
460 mean: f32,
461 scale: f32,
462 key: u64,
463 op_seed: Option<f32>,
464 },
465 RngUniformHost {
467 dst_byte_off: u32,
468 len: u32,
469 low: f32,
470 high: f32,
471 key: u64,
472 op_seed: Option<f32>,
473 },
474 BufferCopy {
478 src_byte_off: u64,
481 dst_byte_off: u64,
482 bytes: u32,
483 },
484 Copy {
485 params: CopyParams,
486 },
487 ElementwiseRegion {
493 params: ElementwiseRegionParams,
494 },
495 BatchElementwiseRegion {
496 params: BatchElementwiseRegionParams,
497 },
498 Transpose {
499 params: TransposeParams,
500 meta_idx: usize,
501 },
502 Narrow {
503 params: NarrowConcatParams,
504 },
505 Concat {
506 params: NarrowConcatParams,
507 }, Gather {
509 params: GatherParams,
510 },
511 GatherAxis {
512 params: GatherAxisParams,
513 },
514 Attention {
515 params: AttentionParams,
516 mask_buf: Option<wgpu::Buffer>,
517 },
518 AttentionBackward {
519 params: AttentionBwdParams,
520 mask_buf: Option<wgpu::Buffer>,
521 },
522 Rope {
523 params: RopeParams,
524 },
525 Expand {
526 params: ExpandParams,
527 meta_idx: usize,
528 },
529 Argmax {
530 params: ArgmaxParams,
531 },
532 Pool2d {
533 params: Pool2dParams,
534 },
535 Conv2d {
536 params: Conv2dParams,
537 },
538 Im2ColGpu {
542 params: Im2Col2dParams,
543 },
544 Conv2dTiled {
548 params: Conv2dParams,
549 },
550 Pool1d {
551 params: Pool1dParams,
552 },
553 Pool3d {
554 params: Pool3dParams,
555 },
556 Conv1d {
557 params: Conv1dParams,
558 },
559 Conv3d {
560 params: Conv3dParams,
561 },
562 ScatterAdd {
563 params: ScatterAddParams,
564 },
565 TopK {
566 params: TopKParams,
567 },
568 WelchPeaksGpu {
569 params: WelchPeaksGpuParams,
570 },
571 GroupedMatmul {
572 params: GroupedMatmulParams,
573 },
574 Sample {
575 params: SampleParams,
576 },
577 SelectiveScan {
578 params: SelectiveScanParams,
579 },
580 Mamba2 {
582 params: Mamba2Params,
583 },
584 Gru {
586 params: GruParams,
587 },
588 Rnn {
590 params: RnnParams,
591 },
592 GruHost {
594 x: u32,
595 w_ih: u32,
596 w_hh: u32,
597 b_ih: u32,
598 b_hh: u32,
599 h0: u32,
600 dst: u32,
601 batch: u32,
602 seq: u32,
603 input_size: u32,
604 hidden: u32,
605 num_layers: u32,
606 bidirectional: bool,
607 carry: bool,
608 },
609 RnnHost {
611 x: u32,
612 w_ih: u32,
613 w_hh: u32,
614 bias: u32,
615 h0: u32,
616 dst: u32,
617 batch: u32,
618 seq: u32,
619 input_size: u32,
620 hidden: u32,
621 num_layers: u32,
622 bidirectional: bool,
623 carry: bool,
624 relu: bool,
625 },
626 DequantMatmul {
627 params: DequantMatmulParams,
628 },
629 GatherSplit {
635 n_out: u32,
636 n_idx: u32,
637 dim: u32,
638 vocab: u32,
639 table_byte_off: u64,
640 idx_byte_off: u64,
641 out_byte_off: u64,
642 },
643 DequantMatmulGguf {
645 m: u32,
646 k: u32,
647 n: u32,
648 scheme_id: u32,
649 x_byte_off: u64,
653 w_byte_off: u64,
654 out_byte_off: u64,
655 },
656 DequantGroupedMatmulGguf {
658 m: u32,
659 k: u32,
660 n: u32,
661 num_experts: u32,
662 scheme_id: u32,
663 x_byte_off: u64,
664 w_byte_off: u64,
665 idx_byte_off: u64,
666 out_byte_off: u64,
667 },
668 GatedDeltaNet {
670 q_byte_off: u32,
671 k_byte_off: u32,
672 v_byte_off: u32,
673 g_byte_off: u32,
674 beta_byte_off: u32,
675 state_byte_off: u32,
676 dst_byte_off: u32,
677 batch: u32,
678 seq: u32,
679 heads: u32,
680 state_size: u32,
681 use_carry: bool,
682 },
683 Lstm {
684 x_byte_off: u32,
685 w_ih_byte_off: u32,
686 w_hh_byte_off: u32,
687 bias_byte_off: u32,
688 h0_byte_off: u32,
689 c0_byte_off: u32,
690 dst_byte_off: u32,
691 batch: u32,
692 seq: u32,
693 input_size: u32,
694 hidden: u32,
695 num_layers: u32,
696 bidirectional: bool,
697 carry: bool,
698 },
699 ConvTranspose2d {
700 src_byte_off: u32,
701 weight_byte_off: u32,
702 dst_byte_off: u32,
703 n: u32,
704 c_in: u32,
705 h: u32,
706 w_in: u32,
707 c_out: u32,
708 h_out: u32,
709 w_out: u32,
710 kh: u32,
711 kw: u32,
712 sh: u32,
713 sw: u32,
714 ph: u32,
715 pw: u32,
716 dh: u32,
717 dw: u32,
718 groups: u32,
719 },
720 GroupNormHost {
722 src_byte_off: u32,
723 gamma_byte_off: u32,
724 beta_byte_off: u32,
725 dst_byte_off: u32,
726 n: u32,
727 c: u32,
728 h: u32,
729 w: u32,
730 num_groups: u32,
731 eps: f32,
732 },
733 LayerNorm2dHost {
735 src_byte_off: u32,
736 gamma_byte_off: u32,
737 beta_byte_off: u32,
738 dst_byte_off: u32,
739 n: u32,
740 c: u32,
741 h: u32,
742 w: u32,
743 eps: f32,
744 },
745 ResizeNearest2xHost {
747 src_byte_off: u32,
748 dst_byte_off: u32,
749 n: u32,
750 c: u32,
751 h: u32,
752 w: u32,
753 },
754 ReverseHost {
756 src_byte_off: u32,
757 dst_byte_off: u32,
758 dims: Vec<u32>,
759 rev_mask: Vec<bool>,
760 elem_bytes: u32,
761 },
762 ArgReduceHost {
764 src_byte_off: u32,
765 dst_byte_off: u32,
766 outer: u32,
767 reduced: u32,
768 inner: u32,
769 is_max: bool,
770 },
771 Llada2GroupLimitedGate {
772 sig_byte_off: u32,
773 route_byte_off: u32,
774 out_byte_off: u32,
775 n_elems: u32,
776 attrs: [u8; 20],
777 },
778 UmapKnn {
779 params: UmapKnnParams,
780 },
781 UmapKnnHost {
783 pairwise_byte_off: u32,
784 out_byte_off: u32,
785 n: u32,
786 k: u32,
787 },
788 MsDeformAttnHost {
790 in_offs: Vec<(u32, u32)>, out_byte_off: u32,
792 out_bytes: u32,
793 attrs: Vec<u8>,
794 },
795 #[cfg(feature = "splat")]
797 GaussianSplatRender {
798 positions_byte_off: u32,
799 positions_len: u32,
800 scales_byte_off: u32,
801 scales_len: u32,
802 rotations_byte_off: u32,
803 rotations_len: u32,
804 opacities_byte_off: u32,
805 opacities_len: u32,
806 colors_byte_off: u32,
807 colors_len: u32,
808 sh_coeffs_byte_off: u32,
809 sh_coeffs_len: u32,
810 meta_byte_off: u32,
811 dst_byte_off: u32,
812 dst_len: u32,
813 width: u32,
814 height: u32,
815 tile_size: u32,
816 radius_scale: f32,
817 alpha_cutoff: f32,
818 max_splat_steps: u32,
819 transmittance_threshold: f32,
820 max_list_entries: u32,
821 },
822 #[cfg(feature = "splat")]
824 GaussianSplatRenderBackward {
825 positions_byte_off: u32,
826 positions_len: u32,
827 scales_byte_off: u32,
828 scales_len: u32,
829 rotations_byte_off: u32,
830 rotations_len: u32,
831 opacities_byte_off: u32,
832 opacities_len: u32,
833 colors_byte_off: u32,
834 colors_len: u32,
835 sh_coeffs_byte_off: u32,
836 sh_coeffs_len: u32,
837 meta_byte_off: u32,
838 d_loss_byte_off: u32,
839 d_loss_len: u32,
840 packed_byte_off: u32,
841 packed_len: u32,
842 width: u32,
843 height: u32,
844 tile_size: u32,
845 radius_scale: f32,
846 alpha_cutoff: f32,
847 max_splat_steps: u32,
848 transmittance_threshold: f32,
849 max_list_entries: u32,
850 loss_grad_clip: f32,
851 sh_band: u32,
852 max_anisotropy: f32,
853 },
854 #[cfg(feature = "splat")]
855 GaussianSplatPrepare {
856 positions_byte_off: u32,
857 positions_len: u32,
858 scales_byte_off: u32,
859 scales_len: u32,
860 rotations_byte_off: u32,
861 rotations_len: u32,
862 opacities_byte_off: u32,
863 opacities_len: u32,
864 colors_byte_off: u32,
865 colors_len: u32,
866 sh_coeffs_byte_off: u32,
867 sh_coeffs_len: u32,
868 meta_byte_off: u32,
869 meta_len: u32,
870 prep_byte_off: u32,
871 prep_len: u32,
872 width: u32,
873 height: u32,
874 tile_size: u32,
875 radius_scale: f32,
876 alpha_cutoff: f32,
877 max_splat_steps: u32,
878 transmittance_threshold: f32,
879 max_list_entries: u32,
880 },
881 #[cfg(feature = "splat")]
882 GaussianSplatRasterize {
883 prep_byte_off: u32,
884 prep_len: u32,
885 meta_byte_off: u32,
886 meta_len: u32,
887 dst_byte_off: u32,
888 dst_len: u32,
889 count: u32,
890 width: u32,
891 height: u32,
892 tile_size: u32,
893 alpha_cutoff: f32,
894 max_splat_steps: u32,
895 transmittance_threshold: f32,
896 max_list_entries: u32,
897 },
898 RmsNormBackwardInput {
899 params: RmsNormBwdParams,
900 },
901 RmsNormBackwardGamma {
902 params: RmsNormBwdParams,
903 },
904 RmsNormBackwardBeta {
905 params: RmsNormBwdParams,
906 },
907 LayerNormBackwardInput {
908 params: LayerNormBwdParams,
909 },
910 LayerNormBackwardGammaPartial {
911 params: LayerNormBwdParams,
912 num_workgroups: u32,
913 },
914 LayerNormBackwardGammaReduce {
915 params: LayerNormBwdParams,
916 },
917 RopeBackward {
918 params: RopeBwdParams,
919 },
920 CumsumBackward {
921 params: CumsumBwdParams,
922 },
923 GatherBackward {
924 params: GatherBwdParams,
925 },
926 FusedResidualLn {
927 params: FusedResidualLnParams,
928 },
929 MatmulQkv {
934 params: MatmulQkvParams,
935 kind: MatmulQkvKind,
936 },
937 FusedResidualLnTee {
941 params: FusedResidualLnTeeParams,
942 },
943 FusedResidualRmsNorm {
944 params: FusedResidualRmsNormParams,
945 },
946}
947
948pub struct WgpuExecutable {
949 graph: Graph,
950 arena: Arena,
951 dequant_scratch_off: usize,
953 schedule: Vec<Step>,
954 input_offsets: HashMap<String, NodeId>,
955 param_offsets: HashMap<String, NodeId>,
956 uniforms: Vec<wgpu::Buffer>,
959 bind_groups: Vec<wgpu::BindGroup>,
960 meta_buffers: Vec<wgpu::Buffer>,
963
964 unresolved: Option<Graph>,
971 last_binding: Option<DimBinding>,
972 pending_params: HashMap<String, Vec<f32>>,
976 pending_param_bytes: HashMap<String, Vec<u8>>,
977 pub(crate) active_extent: Option<(usize, usize)>,
981 uniforms_active_extent: Option<Option<(usize, usize)>>,
991 input_staging_hashes: HashMap<String, u64>,
993 coop_f16_vk: bool,
996 coop_f16_b_param: HashMap<u32, String>,
998 coop_f16_vk_wide_b: HashSet<String>,
1000 coop_f16_vk_wide_bind_groups: HashMap<usize, wgpu::BindGroup>,
1002 coop_f16_host_activations: Vec<(NodeId, Activation, String)>,
1004 stashed_params: HashMap<String, Vec<f32>>,
1006 readback_staging: Option<ReadbackStaging>,
1008 tiny_readback: Option<TinyReadbackStaging>,
1010 dispatch_only: bool,
1015 fft_gpu_steps: Vec<crate::fft_dispatch::FftGpuResources>,
1017 gpu_handles: HashMap<String, Vec<f32>>,
1019 gpu_handle_feeds: HashMap<String, usize>,
1020 gpu_handle_resident: HashSet<String>,
1022 pending_read_indices: Option<Vec<usize>>,
1023 rng: std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
1025}
1026
1027impl Step {
1028 pub fn safe_for_active_extent(&self) -> bool {
1034 match self {
1035 Step::Binary { .. }
1036 | Step::Compare { .. }
1037 | Step::Unary { .. }
1038 | Step::Where { .. }
1039 | Step::Fma { .. }
1040 | Step::Reduce { .. }
1041 | Step::Softmax { .. }
1042 | Step::SoftmaxCrossEntropy { .. }
1043 | Step::LayerNorm { .. }
1044 | Step::FusedResidualLn { .. }
1045 | Step::FusedResidualLnTee { .. }
1046 | Step::FusedResidualRmsNorm { .. }
1047 | Step::Cumsum { .. }
1048 | Step::Copy { .. }
1049 | Step::ElementwiseRegion { .. }
1050 | Step::BatchElementwiseRegion { .. }
1051 | Step::Argmax { .. }
1052 | Step::TopK { .. }
1053 | Step::WelchPeaksGpu { .. }
1054 | Step::Sample { .. }
1055 | Step::Gather { .. }
1056 | Step::GatherAxis { .. }
1057 | Step::GatherSplit { .. }
1058 | Step::GroupedMatmul { .. }
1059 | Step::DequantMatmul { .. }
1060 | Step::DequantMatmulGguf { .. }
1061 | Step::DequantGroupedMatmulGguf { .. }
1062 | Step::GatedDeltaNet { .. }
1063 | Step::Lstm { .. }
1064 | Step::ConvTranspose2d { .. }
1065 | Step::GroupNormHost { .. }
1066 | Step::LayerNorm2dHost { .. }
1067 | Step::ResizeNearest2xHost { .. }
1068 | Step::ReverseHost { .. }
1069 | Step::ArgReduceHost { .. }
1070 | Step::GruHost { .. }
1071 | Step::RnnHost { .. }
1072 | Step::Llada2GroupLimitedGate { .. }
1073 | Step::UmapKnn { .. }
1074 | Step::UmapKnnHost { .. }
1075 | Step::MsDeformAttnHost { .. }
1076 | Step::Conv1d { .. }
1077 | Step::Conv2d { .. }
1078 | Step::Conv2dTiled { .. }
1079 | Step::Conv3d { .. }
1080 | Step::Pool1d { .. }
1081 | Step::Pool2d { .. }
1082 | Step::Pool3d { .. }
1083 | Step::ScatterAdd { .. }
1084 | Step::BufferCopy { .. } => true,
1085 Step::FftGpu { .. } | Step::FftHost { .. } | Step::ScanHost { .. } => true,
1090 Step::Im2ColHost { .. }
1091 | Step::RngNormalHost { .. }
1092 | Step::RngUniformHost { .. }
1093 | Step::WelchPeaksHost { .. }
1094 | Step::LogMelHost { .. }
1095 | Step::LogMelBackwardHost { .. } => true,
1096 Step::Matmul { .. } => true,
1101 Step::Im2ColGpu { .. } => false,
1106 Step::MatmulQkv { .. } => true,
1110 Step::CastF32ToF16 { .. } => true,
1111 Step::Attention { .. } => true,
1117 Step::AttentionBackward { .. } => true,
1118 Step::SelectiveScan { .. } => true,
1123 Step::Mamba2 { .. } => true,
1125 Step::Gru { .. } => true,
1128 Step::Rnn { .. } => true,
1129 Step::Narrow { .. } => true,
1138 Step::Concat { .. } => true,
1139 Step::Rope { .. } => true,
1145 Step::Transpose { params, .. } => params.bucket_outermost == 1,
1151 Step::Expand { params, .. } => params.bucket_outermost == 1,
1155 Step::RmsNormBackwardInput { .. }
1158 | Step::RmsNormBackwardGamma { .. }
1159 | Step::RmsNormBackwardBeta { .. }
1160 | Step::LayerNormBackwardInput { .. }
1161 | Step::LayerNormBackwardGammaPartial { .. }
1162 | Step::LayerNormBackwardGammaReduce { .. }
1163 | Step::RopeBackward { .. }
1164 | Step::CumsumBackward { .. }
1165 | Step::GatherBackward { .. } => false,
1166 #[cfg(feature = "splat")]
1167 Step::GaussianSplatRender { .. }
1168 | Step::GaussianSplatRenderBackward { .. }
1169 | Step::GaussianSplatPrepare { .. }
1170 | Step::GaussianSplatRasterize { .. } => false,
1171 }
1172 }
1173}
1174
1175fn fft_dtype_tag(dtype: rlx_ir::DType) -> u32 {
1178 match dtype {
1179 rlx_ir::DType::F32 => 0,
1180 rlx_ir::DType::F64 => 1,
1181 rlx_ir::DType::C64 => 2,
1182 other => panic!("rlx-wgpu Op::Fft: unsupported dtype {other:?}"),
1183 }
1184}
1185
1186fn fft_dtype_from_tag(tag: u32) -> rlx_ir::DType {
1187 match tag {
1188 0 => rlx_ir::DType::F32,
1189 1 => rlx_ir::DType::F64,
1190 2 => rlx_ir::DType::C64,
1191 other => panic!("rlx-wgpu Op::Fft: bad dtype tag {other}"),
1192 }
1193}
1194
1195fn step_name(step: &Step) -> &'static str {
1196 match step {
1197 Step::CastF32ToF16 { .. } => "cast_f32_to_f16",
1198 Step::Matmul { .. } => "matmul",
1199 Step::Binary { .. } => "binary",
1200 Step::Compare { .. } => "compare",
1201 Step::Unary { .. } => "unary",
1202 Step::Where { .. } => "where",
1203 Step::Fma { .. } => "fma",
1204 Step::Reduce { .. } => "reduce",
1205 Step::Softmax { .. } => "softmax",
1206 Step::SoftmaxCrossEntropy { .. } => "softmax_cross_entropy",
1207 Step::LayerNorm { .. } => "layer_norm",
1208 Step::Cumsum { .. } => "cumsum",
1209 Step::FftGpu { .. } => "fft_gpu",
1210 Step::FftHost { .. } => "fft_host",
1211 Step::WelchPeaksHost { .. } => "welch_peaks_host",
1212 Step::LogMelHost { .. } => "log_mel_host",
1213 Step::LogMelBackwardHost { .. } => "log_mel_backward_host",
1214 Step::Im2ColHost { .. } => "im2col_host",
1215 Step::RngNormalHost { .. } => "rng_normal_host",
1216 Step::RngUniformHost { .. } => "rng_uniform_host",
1217 Step::BufferCopy { .. } => "buffer_copy",
1218 Step::Copy { .. } => "copy",
1219 Step::Transpose { .. } => "transpose",
1220 Step::Narrow { .. } => "narrow",
1221 Step::Concat { .. } => "concat",
1222 Step::Gather { .. } => "gather",
1223 Step::GatherAxis { .. } => "gather_axis",
1224 Step::Attention { .. } => "attention",
1225 Step::AttentionBackward { .. } => "attention_bwd",
1226 Step::Rope { .. } => "rope",
1227 Step::Expand { .. } => "expand",
1228 Step::Argmax { .. } => "argmax",
1229 Step::Pool2d { .. } => "pool2d",
1230 Step::Conv2d { .. } => "conv2d",
1231 Step::Conv2dTiled { .. } => "conv2d_tiled",
1232 Step::Im2ColGpu { .. } => "im2col_gpu",
1233 Step::Pool1d { .. } => "pool1d",
1234 Step::Pool3d { .. } => "pool3d",
1235 Step::Conv1d { .. } => "conv1d",
1236 Step::Conv3d { .. } => "conv3d",
1237 Step::ScatterAdd { .. } => "scatter_add",
1238 Step::TopK { .. } => "topk",
1239 Step::WelchPeaksGpu { .. } => "welch_peaks_gpu",
1240 Step::GroupedMatmul { .. } => "grouped_matmul",
1241 Step::Sample { .. } => "sample",
1242 Step::SelectiveScan { .. } => "selective_scan",
1243 Step::Mamba2 { .. } => "mamba2",
1244 Step::Gru { .. } => "gru",
1245 Step::Rnn { .. } => "rnn",
1246 Step::GruHost { .. } => "gru_host",
1247 Step::RnnHost { .. } => "rnn_host",
1248 Step::DequantMatmul { .. } => "dequant_matmul",
1249 Step::GatherSplit { .. } => "gather_split",
1250 Step::DequantMatmulGguf { .. } => "dequant_matmul_gguf",
1251 Step::DequantGroupedMatmulGguf { .. } => "dequant_grouped_matmul_gguf",
1252 Step::GatedDeltaNet { .. } => "gated_delta_net",
1253 Step::Lstm { .. } => "lstm",
1254 Step::ConvTranspose2d { .. } => "conv_transpose2d",
1255 Step::GroupNormHost { .. } => "group_norm_host",
1256 Step::LayerNorm2dHost { .. } => "layer_norm2d_host",
1257 Step::ResizeNearest2xHost { .. } => "resize_nearest2x_host",
1258 Step::ReverseHost { .. } => "reverse_host",
1259 Step::ArgReduceHost { .. } => "argreduce_host",
1260 Step::Llada2GroupLimitedGate { .. } => "llada2_group_limited_gate",
1261 Step::UmapKnn { .. } => "umap_knn",
1262 Step::UmapKnnHost { .. } => "umap_knn_host",
1263 Step::MsDeformAttnHost { .. } => "ms_deform_attn_host",
1264 Step::ScanHost { .. } => "scan_host",
1265 #[cfg(feature = "splat")]
1266 Step::GaussianSplatRender { .. } => "gaussian_splat_render",
1267 #[cfg(feature = "splat")]
1268 Step::GaussianSplatRenderBackward { .. } => "gaussian_splat_render_backward",
1269 #[cfg(feature = "splat")]
1270 Step::GaussianSplatPrepare { .. } => "gaussian_splat_prepare",
1271 #[cfg(feature = "splat")]
1272 Step::GaussianSplatRasterize { .. } => "gaussian_splat_rasterize",
1273 Step::RmsNormBackwardInput { .. } => "rms_norm_backward_input",
1274 Step::RmsNormBackwardGamma { .. } => "rms_norm_backward_gamma",
1275 Step::RmsNormBackwardBeta { .. } => "rms_norm_backward_beta",
1276 Step::LayerNormBackwardInput { .. } => "layer_norm_backward_input",
1277 Step::LayerNormBackwardGammaPartial { .. } => "layer_norm_backward_gamma_partial",
1278 Step::LayerNormBackwardGammaReduce { .. } => "layer_norm_backward_gamma_reduce",
1279 Step::RopeBackward { .. } => "rope_backward",
1280 Step::CumsumBackward { .. } => "cumsum_backward",
1281 Step::GatherBackward { .. } => "gather_backward",
1282 Step::FusedResidualLn { .. } => "fused_residual_ln",
1283 Step::FusedResidualLnTee { .. } => "fused_residual_ln_tee",
1284 Step::FusedResidualRmsNorm { .. } => "fused_residual_rms_norm",
1285 Step::MatmulQkv { .. } => "matmul_qkv",
1286 Step::ElementwiseRegion { .. } => "elementwise_region",
1287 Step::BatchElementwiseRegion { .. } => "batch_elementwise_region",
1288 }
1289}
1290
1291fn step_is_tail_host(step: &Step) -> bool {
1292 matches!(
1293 step,
1294 Step::WelchPeaksHost { .. } | Step::LogMelHost { .. } | Step::LogMelBackwardHost { .. }
1295 )
1296}
1297
1298fn step_runs_on_host(step: &Step) -> bool {
1299 match step {
1300 Step::GatherSplit { .. }
1301 | Step::DequantMatmulGguf { .. }
1302 | Step::DequantGroupedMatmulGguf { .. }
1303 | Step::GatedDeltaNet { .. }
1304 | Step::Lstm { .. }
1305 | Step::ConvTranspose2d { .. }
1306 | Step::GroupNormHost { .. }
1307 | Step::LayerNorm2dHost { .. }
1308 | Step::ResizeNearest2xHost { .. }
1309 | Step::ReverseHost { .. }
1310 | Step::ArgReduceHost { .. }
1311 | Step::GruHost { .. }
1312 | Step::RnnHost { .. }
1313 | Step::Llada2GroupLimitedGate { .. }
1314 | Step::UmapKnnHost { .. }
1315 | Step::MsDeformAttnHost { .. }
1316 | Step::FftHost { .. }
1317 | Step::ScanHost { .. }
1318 | Step::Im2ColHost { .. }
1319 | Step::RngNormalHost { .. }
1320 | Step::RngUniformHost { .. }
1321 | Step::BufferCopy { .. } => true,
1322 #[cfg(feature = "splat")]
1323 Step::GaussianSplatRender { .. }
1324 | Step::GaussianSplatRenderBackward { .. }
1325 | Step::GaussianSplatPrepare { .. }
1326 | Step::GaussianSplatRasterize { .. } => true,
1327 _ => false,
1328 }
1329}
1330
1331fn binary_op_id(op: BinaryOp) -> u32 {
1332 match op {
1333 BinaryOp::Add => 0,
1334 BinaryOp::Sub => 1,
1335 BinaryOp::Mul => 2,
1336 BinaryOp::Div => 3,
1337 BinaryOp::Max => 4,
1338 BinaryOp::Min => 5,
1339 BinaryOp::Pow => 6,
1340 }
1341}
1342
1343fn compare_op_id(op: CmpOp) -> u32 {
1344 match op {
1345 CmpOp::Eq => 0,
1346 CmpOp::Ne => 1,
1347 CmpOp::Lt => 2,
1348 CmpOp::Le => 3,
1349 CmpOp::Gt => 4,
1350 CmpOp::Ge => 5,
1351 }
1352}
1353
1354fn reduce_op_id(op: ReduceOp) -> u32 {
1355 match op {
1356 ReduceOp::Sum => 0,
1357 ReduceOp::Mean => 1,
1358 ReduceOp::Max => 2,
1359 ReduceOp::Min => 3,
1360 ReduceOp::Prod => 4,
1361 }
1362}
1363
1364fn activation_op_id(act: Activation) -> u32 {
1365 match act {
1366 Activation::Relu => 0,
1367 Activation::Sigmoid => 1,
1368 Activation::Tanh => 2,
1369 Activation::Exp => 3,
1370 Activation::Log => 4,
1371 Activation::Sqrt => 5,
1372 Activation::Rsqrt => 6,
1373 Activation::Neg => 7,
1374 Activation::Abs => 8,
1375 Activation::Gelu => 9,
1376 Activation::Silu => 10,
1377 Activation::GeluApprox => 11,
1378 Activation::Round => 12,
1379 Activation::Sin => 13,
1380 Activation::Cos => 14,
1381 Activation::Tan => 15,
1382 Activation::Atan => 16,
1383 }
1384}
1385
1386mod compile;
1387mod dispatch;
1388mod run;
1389mod set;
1390mod test;
1391
1392impl WgpuExecutable {
1393 pub(crate) fn lazy_compile_for_inputs(&mut self, inputs: &[(&str, &[f32])]) {
1397 let unresolved = self
1398 .unresolved
1399 .as_ref()
1400 .expect("lazy_compile_for_inputs called without an unresolved graph");
1401 let binding = infer_bindings_from_f32_inputs(unresolved, inputs)
1402 .expect("rlx-wgpu lazy compile: could not infer DimBinding from inputs");
1403
1404 if let Some(prev) = &self.last_binding
1406 && same_binding(prev, &binding)
1407 {
1408 return;
1409 }
1410
1411 let resolved = bind_graph(unresolved, &binding);
1413 let original = self.unresolved.take();
1414 let pending_params = std::mem::take(&mut self.pending_params);
1415 let pending_bytes = std::mem::take(&mut self.pending_param_bytes);
1416
1417 let fresh = Self::compile_static_inner(resolved, self.rng.clone());
1418
1419 self.graph = fresh.graph;
1422 self.arena = fresh.arena;
1423 self.dequant_scratch_off = fresh.dequant_scratch_off;
1424 self.schedule = fresh.schedule;
1425 self.input_offsets = fresh.input_offsets;
1426 self.param_offsets = fresh.param_offsets;
1427 self.uniforms = fresh.uniforms;
1428 self.bind_groups = fresh.bind_groups;
1429 self.meta_buffers = fresh.meta_buffers;
1430 self.unresolved = original;
1431 self.last_binding = Some(binding);
1432 self.uniforms_active_extent = None;
1435 self.input_staging_hashes.clear();
1436 self.coop_f16_vk = fresh.coop_f16_vk;
1437 self.coop_f16_b_param = fresh.coop_f16_b_param;
1438 self.coop_f16_vk_wide_bind_groups = fresh.coop_f16_vk_wide_bind_groups;
1439 self.coop_f16_host_activations = fresh.coop_f16_host_activations;
1440
1441 for (name, data) in pending_params {
1443 self.set_param(&name, &data);
1444 }
1445 for (name, data) in pending_bytes {
1446 self.set_param_bytes(&name, &data);
1447 }
1448 }
1449
1450 pub fn rng(&self) -> rlx_ir::RngOptions {
1452 *self.rng.read().expect("rng lock")
1453 }
1454
1455 pub(crate) fn deferred(
1460 graph: Graph,
1461 rng: std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
1462 ) -> Self {
1463 let dev = wgpu_device().expect("rlx-wgpu: no compatible adapter found");
1464 let placeholder = dev.device.create_buffer(&wgpu::BufferDescriptor {
1466 label: Some("rlx-wgpu deferred placeholder"),
1467 size: 16,
1468 usage: wgpu::BufferUsages::STORAGE
1469 | wgpu::BufferUsages::COPY_DST
1470 | wgpu::BufferUsages::COPY_SRC,
1471 mapped_at_creation: false,
1472 });
1473 let arena = Arena {
1474 buffer: placeholder,
1475 f16_buffer: None,
1476 offsets: HashMap::new(),
1477 lens: HashMap::new(),
1478 size: 0,
1479 scratch_off: 0,
1480 scratch_bytes: 0,
1481 };
1482 Self {
1483 graph: graph.clone(),
1484 arena,
1485 dequant_scratch_off: 0,
1486 schedule: Vec::new(),
1487 input_offsets: HashMap::new(),
1488 param_offsets: HashMap::new(),
1489 uniforms: Vec::new(),
1490 bind_groups: Vec::new(),
1491 meta_buffers: Vec::new(),
1492 unresolved: Some(graph),
1493 last_binding: None,
1494 pending_params: HashMap::new(),
1495 pending_param_bytes: HashMap::new(),
1496 active_extent: None,
1497 uniforms_active_extent: None,
1498 input_staging_hashes: HashMap::new(),
1499 coop_f16_vk: false,
1500 coop_f16_b_param: HashMap::new(),
1501 coop_f16_vk_wide_b: HashSet::new(),
1502 coop_f16_vk_wide_bind_groups: HashMap::new(),
1503 coop_f16_host_activations: Vec::new(),
1504 stashed_params: HashMap::new(),
1505 readback_staging: None,
1506 tiny_readback: None,
1507 dispatch_only: false,
1508 fft_gpu_steps: Vec::new(),
1509 gpu_handles: HashMap::new(),
1510 gpu_handle_feeds: HashMap::new(),
1511 gpu_handle_resident: HashSet::new(),
1512 pending_read_indices: None,
1513 rng,
1514 }
1515 }
1516
1517 pub(crate) fn all_safe_for_active(&self) -> bool {
1518 self.schedule.iter().all(|s| s.safe_for_active_extent())
1519 }
1520
1521 pub fn debug_first_nan_node(
1526 &mut self,
1527 inputs: &[(&str, &[f32])],
1528 ) -> Option<(usize, String, String)> {
1529 let _ = self.run(inputs);
1530 let dev = wgpu_device().expect("rlx-wgpu: device gone");
1531 let mut prev_summary = String::from("(none)");
1532 for (i, node) in self.graph.nodes().iter().enumerate() {
1533 if !self.arena.has(node.id) {
1534 continue;
1535 }
1536 let elems = node.shape.num_elements().unwrap_or(0);
1537 if elems == 0 {
1538 continue;
1539 }
1540 let data = self.arena.read_f32(&dev.device, &dev.queue, node.id);
1541 let nan_count = data.iter().filter(|v| v.is_nan()).count();
1542 let inf_count = data.iter().filter(|v| v.is_infinite()).count();
1543 if nan_count > 0 || inf_count > 0 {
1544 return Some((i, format!("{:?}", node.op), prev_summary));
1545 }
1546 let max = data.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1547 let min = data.iter().copied().fold(f32::INFINITY, f32::min);
1548 let abs_max = data.iter().map(|v| v.abs()).fold(0.0_f32, f32::max);
1549 prev_summary = format!(
1550 "node #{i} {:?} shape={:?} min={min:.6e} max={max:.6e} |max|={abs_max:.6e}",
1551 node.op,
1552 node.shape
1553 .dims()
1554 .iter()
1555 .map(|d| format!("{d:?}"))
1556 .collect::<Vec<_>>()
1557 );
1558 }
1559 None
1560 }
1561
1562 pub fn output_dtypes(&self) -> Vec<rlx_ir::DType> {
1566 self.graph
1567 .outputs
1568 .iter()
1569 .map(|&id| self.graph.node(id).shape.dtype())
1570 .collect()
1571 }
1572
1573 pub(crate) fn dump_node_stats_if_requested(&self, dev: &crate::device::WgpuDevice) {
1574 if !rlx_ir::env::flag("RLX_WGPU_DUMP_NODES") {
1575 return;
1576 }
1577 let flat_probe = rlx_ir::env::parse_or::<usize>("RLX_WGPU_DUMP_FLAT", usize::MAX);
1578 let limit = rlx_ir::env::parse_or("RLX_WGPU_DUMP_NODES_LIMIT", 40usize);
1579 eprintln!(
1580 "[rlx-wgpu-dump] per-node max |x| (topo order, limit={limit}{})",
1581 if flat_probe != usize::MAX {
1582 format!(", flat[{flat_probe}]")
1583 } else {
1584 String::new()
1585 }
1586 );
1587 let mut shown = 0usize;
1588 for (i, node) in self.graph.nodes().iter().enumerate() {
1589 if !self.arena.has(node.id) {
1590 continue;
1591 }
1592 if matches!(
1593 node.op,
1594 rlx_ir::Op::Input { .. }
1595 | rlx_ir::Op::Param { .. }
1596 | rlx_ir::Op::Constant { .. }
1597 | rlx_ir::Op::Reshape { .. }
1598 | rlx_ir::Op::Cast { .. }
1599 ) {
1600 continue;
1601 }
1602 let data = self.arena.read_f32(&dev.device, &dev.queue, node.id);
1603 let max = data.iter().fold(0.0f32, |m, &v| m.max(v.abs()));
1604 let nz = data.iter().filter(|&&v| v != 0.0).count();
1605 let flat_s = if flat_probe < data.len() {
1606 format!(" flat[{flat_probe}]={:.6}", data[flat_probe])
1607 } else {
1608 String::new()
1609 };
1610 eprintln!(
1611 " [{i:>3}] {:?} max={max:.6} nonzero={}/{}{flat_s}",
1612 node.op,
1613 nz,
1614 data.len()
1615 );
1616 shown += 1;
1617 if shown >= limit {
1618 break;
1619 }
1620 }
1621 }
1622
1623 pub fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
1624 if !self.input_offsets.contains_key(name) {
1625 return false;
1626 }
1627 self.gpu_handle_resident.remove(name);
1628 self.gpu_handles.insert(name.to_string(), data.to_vec());
1629 true
1630 }
1631
1632 pub fn has_gpu_handle(&self, name: &str) -> bool {
1633 self.gpu_handles.contains_key(name)
1634 }
1635
1636 pub fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
1637 if let Some(&out_idx) = self.gpu_handle_feeds.get(name) {
1638 if out_idx < self.graph.outputs.len() {
1639 let id = self.graph.outputs[out_idx];
1640 if self.arena.has(id) {
1641 let dev = wgpu_device().expect("rlx-wgpu: device gone");
1642 return Some(self.arena.read_f32(&dev.device, &dev.queue, id));
1643 }
1644 }
1645 }
1646 if self.gpu_handle_resident.contains(name) {
1647 if let Some(&id) = self.input_offsets.get(name) {
1648 if self.arena.has(id) {
1649 let dev = wgpu_device().expect("rlx-wgpu: device gone");
1650 return Some(self.arena.read_f32(&dev.device, &dev.queue, id));
1651 }
1652 }
1653 }
1654 self.gpu_handles.get(name).cloned()
1655 }
1656
1657 pub fn clone_for_cache(&self) -> Self {
1659 let graph = self
1660 .unresolved
1661 .clone()
1662 .unwrap_or_else(|| self.graph.clone());
1663 let mut exe = Self::compile_rng(graph, self.rng());
1664 for (k, v) in &self.stashed_params {
1665 exe.set_param(k, v);
1666 }
1667 for (k, v) in &self.pending_params {
1668 exe.set_param(k, v);
1669 }
1670 for (k, v) in &self.pending_param_bytes {
1671 exe.set_param_bytes(k, v);
1672 }
1673 for (k, v) in &self.gpu_handles {
1674 exe.bind_gpu_handle(k, v);
1675 }
1676 for (k, &idx) in &self.gpu_handle_feeds {
1677 exe.set_gpu_handle_feed(k, idx);
1678 }
1679 exe.set_active_extent(self.active_extent);
1680 exe.set_rng(self.rng());
1681 exe
1682 }
1683
1684 pub(crate) fn readback_plan(&self) -> Vec<usize> {
1685 let n = self.graph.outputs.len();
1686 if self.pending_read_indices.is_none() && self.gpu_handle_feeds.is_empty() {
1687 return (0..n).collect();
1688 }
1689 if let Some(ref want) = self.pending_read_indices {
1690 let mut v: Vec<_> = want.to_vec();
1691 v.sort_unstable();
1692 return v;
1693 }
1694 (0..n).collect()
1695 }
1696
1697 pub(crate) fn propagate_gpu_handle_feeds_on_gpu(
1698 &mut self,
1699 dev: &crate::device::WgpuDevice,
1700 enc: &mut wgpu::CommandEncoder,
1701 ) {
1702 let extent = self.active_extent;
1703 let feeds: Vec<(String, usize)> = self
1704 .gpu_handle_feeds
1705 .iter()
1706 .map(|(n, &i)| (n.clone(), i))
1707 .collect();
1708 for (name, out_idx) in feeds {
1709 if out_idx >= self.graph.outputs.len() {
1710 continue;
1711 }
1712 let out_id = self.graph.outputs[out_idx];
1713 let Some(&in_id) = self.input_offsets.get(name.as_str()) else {
1714 continue;
1715 };
1716 if in_id != out_id {
1717 let out_bytes = self.arena.len_of(out_id);
1718 let copy_bytes = match extent {
1719 Some((actual, upper)) if upper > 0 => {
1720 let stride = (out_bytes / (upper + 1)).max(4);
1721 (actual * stride).min(out_bytes)
1722 }
1723 _ => out_bytes,
1724 };
1725 self.dispatch_arena_copy_bytes(dev, enc, out_id, in_id, copy_bytes);
1726 }
1727 self.gpu_handle_resident.insert(name.clone());
1728 self.gpu_handles.insert(name.clone(), Vec::new());
1729 }
1730 }
1731
1732 pub(crate) fn stage_gpu_handle_inputs(
1733 &mut self,
1734 dev: &crate::device::WgpuDevice,
1735 inputs: &[(&str, &[f32])],
1736 ) {
1737 for (name, data) in &self.gpu_handles {
1738 if self.gpu_handle_resident.contains(name) || inputs.iter().any(|(n, _)| n == name) {
1739 continue;
1740 }
1741 if let Some(&id) = self.input_offsets.get(name.as_str())
1742 && self.arena.has(id)
1743 {
1744 self.arena.write_f32(&dev.queue, id, data);
1745 self.input_staging_hashes.remove(name);
1746 }
1747 }
1748 }
1749
1750 pub(crate) fn pack_readback_outputs(
1751 &mut self,
1752 plan: &[usize],
1753 partial: Vec<Vec<f32>>,
1754 ) -> Vec<Vec<f32>> {
1755 if self.pending_read_indices.is_none() {
1756 for (pos, &out_i) in plan.iter().enumerate() {
1757 if let Some(data) = partial.get(pos) {
1758 for (name, &feed_i) in &self.gpu_handle_feeds {
1759 if feed_i == out_i {
1760 self.gpu_handles.insert(name.clone(), data.clone());
1761 }
1762 }
1763 }
1764 }
1765 }
1766 if self.pending_read_indices.is_none() && plan.len() == self.graph.outputs.len() {
1767 return partial;
1768 }
1769 let want = self.pending_read_indices.as_deref().unwrap_or(plan);
1770 let mut by_idx = std::collections::HashMap::new();
1771 for (pos, &i) in plan.iter().enumerate() {
1772 if let Some(d) = partial.get(pos) {
1773 by_idx.insert(i, d.clone());
1774 }
1775 }
1776 want.iter()
1777 .map(|&i| {
1778 by_idx
1779 .get(&i)
1780 .cloned()
1781 .expect("readback plan missing output")
1782 })
1783 .collect()
1784 }
1785}
1786
1787fn dispatch_prologue_nchw(w: u32, h: u32, nc: u32) -> (u32, u32, u32) {
1794 (w.div_ceil(8).max(1), h.div_ceil(8).max(1), nc.max(1))
1795}
1796
1797fn dispatch_dims(threads_total: u32, workgroup_size: u32) -> (u32, u32, u32) {
1798 let groups = threads_total.div_ceil(workgroup_size);
1799 if groups <= 65535 {
1800 (groups, 1, 1)
1801 } else {
1802 let gx = 65535u32;
1803 let gy = groups.div_ceil(gx);
1804 (gx, gy, 1)
1805 }
1806}
1807
1808fn coop_f16_vk_eligible(dev: &wgpu::Device, m: u32, k: u32, n: u32) -> bool {
1830 if rlx_ir::env::flag("RLX_WGPU_NO_COOP_F16_VK")
1831 || rlx_ir::env::flag("RLX_WGPU_COOP_F16_VK_DISABLE")
1832 {
1833 return false;
1834 }
1835 if !rlx_ir::env::flag("RLX_WGPU_COOP_F16_VK_ENABLE") {
1836 return false;
1837 }
1838 m.is_multiple_of(16)
1839 && k.is_multiple_of(16)
1840 && n.is_multiple_of(16)
1841 && dev
1842 .features()
1843 .contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX)
1844 && dev.features().contains(wgpu::Features::SHADER_F16)
1845 && crate::device::coop_discrete_backend()
1846 && crate::device::coop_f16_16x16_supported()
1847}
1848
1849fn step_needs_pass_flush(step: &Step, prev: &Step) -> bool {
1850 match step {
1851 Step::CastF32ToF16 { .. } => matches!(
1852 prev,
1853 Step::Unary {
1854 f16_mirror: false,
1855 ..
1856 }
1857 ),
1858 Step::Matmul {
1859 compute_precision: MatmulCompute::CoopF16Vk,
1860 ..
1861 }
1862 | Step::MatmulQkv {
1863 kind: MatmulQkvKind::CoopF16Vk,
1864 ..
1865 } => matches!(prev, Step::Unary { .. } | Step::CastF32ToF16 { .. }),
1866 _ => false,
1867 }
1868}
1869
1870fn dispatch_wide_f32_matmul(
1871 pass: &mut wgpu::ComputePass<'_>,
1872 mm_w_active: &Kernel,
1873 mm_k: &Kernel,
1874 m_s: u32,
1875 n: u32,
1876 batch: u32,
1877) {
1878 let backend = wgpu_device()
1893 .map(|d| d.backend)
1894 .unwrap_or(wgpu::Backend::Noop);
1895 let is_vulkan_dx12 = matches!(backend, wgpu::Backend::Vulkan | wgpu::Backend::Dx12);
1896 let prefer_small_for_m = is_vulkan_dx12 && m_s < 64;
1897 let use_wide = !prefer_small_for_m && m_s >= 32 && n >= 64;
1898 if use_wide {
1899 pass.set_pipeline(&mm_w_active.pipeline);
1900 let (gx, gy) = if is_vulkan_dx12 {
1901 (n.div_ceil(64), m_s.div_ceil(64))
1902 } else {
1903 (n.div_ceil(64), m_s.div_ceil(32))
1904 };
1905 pass.dispatch_workgroups(gx, gy, batch);
1906 } else {
1907 pass.set_pipeline(&mm_k.pipeline);
1908 pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), batch);
1909 }
1910}
1911
1912fn coop_f16_vk_bind_group(exe: &WgpuExecutable, gpu_bi: usize, use_wide: bool) -> &wgpu::BindGroup {
1913 if use_wide {
1914 exe.coop_f16_vk_wide_bind_groups
1915 .get(&gpu_bi)
1916 .unwrap_or(&exe.bind_groups[gpu_bi])
1917 } else {
1918 &exe.bind_groups[gpu_bi]
1919 }
1920}
1921
1922fn require_equal_shapes(graph: &Graph, ids: &[NodeId], op_name: &str) {
1923 let s0 = graph.node(ids[0]).shape.num_elements().unwrap_or(0);
1924 for &id in &ids[1..] {
1925 let si = graph.node(id).shape.num_elements().unwrap_or(0);
1926 if si != s0 {
1927 panic!(
1928 "rlx-wgpu {op_name}: broadcasting not yet implemented; \
1929 inputs must have the same element count (got {s0} vs {si})"
1930 );
1931 }
1932 }
1933}
1934
1935fn arena_whole_arena_bind(arena: &Arena, max_binding: u64) -> Option<(u64, u64)> {
1937 let need = arena.size as u64;
1938 if need > max_binding {
1939 return None;
1940 }
1941 let buf_bytes = arena.buffer.size();
1943 let size = need.min(buf_bytes).max(256);
1944 Some((0, size))
1945}
1946
1947fn arena_window_for_nodes(dev: &wgpu::Device, arena: &Arena, ids: &[NodeId]) -> (u64, u64) {
1948 const ALIGN: u64 = 256;
1950 let max_binding = dev.limits().max_storage_buffer_binding_size;
1951 if let Some(w) = arena_whole_arena_bind(arena, max_binding) {
1952 return w;
1953 }
1954 let mut lo: u64 = u64::MAX;
1955 let mut hi: u64 = 0;
1956 for &id in ids {
1957 let off = arena.offset(id) as u64;
1958 let len = arena.len_of(id) as u64;
1959 lo = lo.min(off);
1960 hi = hi.max(off.saturating_add(len));
1961 }
1962 if lo == u64::MAX {
1963 return (0, max_binding.max(256));
1964 }
1965 let span = hi.saturating_sub(lo).max(1);
1966 if span > max_binding {
1967 let mut details = String::new();
1968 for &id in ids.iter().take(6) {
1969 let off = arena.offset(id);
1970 let len = arena.len_of(id);
1971 details.push_str(&format!(" id={id:?}@{off}+{len};"));
1972 }
1973 panic!(
1974 "rlx-wgpu: op needs {} bytes of arena span (>{});{}",
1975 span, max_binding, details
1976 );
1977 }
1978 let mut base = (lo / ALIGN) * ALIGN;
1979 let mut size = span.div_ceil(ALIGN) * ALIGN;
1982 size = size.max(256).min(max_binding);
1983 if base.saturating_add(size) > arena.size as u64 {
1984 base = (arena.size as u64).saturating_sub(size);
1985 base = (base / ALIGN) * ALIGN;
1986 }
1987 if base > lo || base.saturating_add(size) < hi {
1988 base = (lo / ALIGN) * ALIGN;
1989 size = hi.saturating_sub(base).div_ceil(ALIGN) * ALIGN;
1990 size = size.max(256).min(max_binding);
1991 if base.saturating_add(size) > arena.size as u64 {
1992 base = hi.saturating_sub(size);
1993 base = (base / ALIGN) * ALIGN;
1994 }
1995 }
1996 (base, size)
1997}
1998
1999fn arena_local_off_f32(arena: &Arena, id: NodeId, base: u64) -> u32 {
2000 (((arena.offset(id) as u64).saturating_sub(base)) / 4) as u32
2001}
2002
2003#[allow(clippy::too_many_arguments)]
2012fn run_gather_split(
2013 arena: &Arena,
2014 device: &wgpu::Device,
2015 queue: &wgpu::Queue,
2016 n_out: u32,
2017 n_idx: u32,
2018 dim: u32,
2019 vocab: u32,
2020 table_byte_off: usize,
2021 idx_byte_off: usize,
2022 out_byte_off: usize,
2023) {
2024 const ALIGN: u64 = 256;
2025 let arena_size = arena.size as u64;
2026 let max_bind = device.limits().max_storage_buffer_binding_size;
2027
2028 let t0 = table_byte_off as u64;
2030 let t_bytes = (vocab as u64) * (dim as u64) * 4;
2031 let t_base = (t0 / ALIGN) * ALIGN;
2032 let t_size = ((t0 + t_bytes - t_base).div_ceil(16) * 16).min(arena_size - t_base);
2033
2034 let i0 = idx_byte_off as u64;
2036 let i_bytes = ((n_idx as u64) * 4).max(4);
2037 let i_base = (i0 / ALIGN) * ALIGN;
2038 let i_size = ((i0 + i_bytes - i_base).div_ceil(16) * 16).min(arena_size - i_base);
2039
2040 assert!(
2041 t_size <= max_bind && i_size <= max_bind,
2042 "rlx-wgpu gather_split: window too large (table={t_size}, idx={i_size}, max={max_bind})"
2043 );
2044
2045 let out_bytes = ((n_out as u64) * 4).max(4);
2047 let out_buf = device.create_buffer(&wgpu::BufferDescriptor {
2048 label: Some("rlx-wgpu gather_split out"),
2049 size: out_bytes.div_ceil(16) * 16,
2050 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
2051 mapped_at_creation: false,
2052 });
2053
2054 let p = GatherParams {
2055 n_out,
2056 n_idx,
2057 dim,
2058 vocab,
2059 in_off: ((t0 - t_base) / 4) as u32,
2060 idx_off: ((i0 - i_base) / 4) as u32,
2061 out_off: 0,
2062 _p0: 0,
2063 };
2064 let u = device.create_buffer(&wgpu::BufferDescriptor {
2065 label: Some("rlx-wgpu gather_split uniform"),
2066 size: std::mem::size_of::<GatherParams>() as u64,
2067 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2068 mapped_at_creation: false,
2069 });
2070 queue.write_buffer(&u, 0, bytemuck::bytes_of(&p));
2071
2072 let gk = gather_split_kernel(device);
2073 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
2074 label: Some("rlx-wgpu gather_split bg"),
2075 layout: &gk.bgl,
2076 entries: &[
2077 wgpu::BindGroupEntry {
2078 binding: 0,
2079 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2080 buffer: &arena.buffer,
2081 offset: t_base,
2082 size: wgpu::BufferSize::new(t_size),
2083 }),
2084 },
2085 wgpu::BindGroupEntry {
2086 binding: 1,
2087 resource: u.as_entire_binding(),
2088 },
2089 wgpu::BindGroupEntry {
2090 binding: 2,
2091 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2092 buffer: &arena.buffer,
2093 offset: i_base,
2094 size: wgpu::BufferSize::new(i_size),
2095 }),
2096 },
2097 wgpu::BindGroupEntry {
2098 binding: 3,
2099 resource: out_buf.as_entire_binding(),
2100 },
2101 ],
2102 });
2103
2104 let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2105 label: Some("rlx-wgpu gather_split"),
2106 });
2107 {
2108 let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
2109 label: Some("rlx-wgpu gather_split pass"),
2110 ..Default::default()
2111 });
2112 pass.set_pipeline(&gk.pipeline);
2113 pass.set_bind_group(0, &bg, &[]);
2114 let (gx, gy, gz) = dispatch_dims(n_out, 64);
2115 pass.dispatch_workgroups(gx, gy, gz);
2116 }
2117 enc.copy_buffer_to_buffer(&out_buf, 0, &arena.buffer, out_byte_off as u64, out_bytes);
2119 queue.submit(std::iter::once(enc.finish()));
2120}
2121
2122fn arena_tensor_in_window(arena: &Arena, id: NodeId, base: u64, size: u64) -> bool {
2123 let src = arena.offset(id) as u64;
2124 let len = arena.len_of(id) as u64;
2125 src >= base && src.saturating_add(len) <= base.saturating_add(size)
2126}
2127
2128fn arena_tensors_overlap(arena: &Arena, a: NodeId, b: NodeId) -> bool {
2130 if a == b {
2131 return true;
2132 }
2133 let (a0, al) = (arena.offset(a) as u64, arena.len_of(a) as u64);
2134 let (b0, bl) = (arena.offset(b) as u64, arena.len_of(b) as u64);
2135 if al == 0 || bl == 0 {
2136 return false;
2137 }
2138 let a1 = a0.saturating_add(al);
2139 let b1 = b0.saturating_add(bl);
2140 a0 < b1 && b0 < a1
2141}
2142
2143fn matmul_b_from_f16(precision: MatmulCompute, b_is_param: bool) -> bool {
2147 b_is_param
2148 && matches!(
2149 precision,
2150 MatmulCompute::F16 | MatmulCompute::Coop16 | MatmulCompute::CoopF16Vk
2151 )
2152}
2153
2154fn arena_matmul_bind_window(
2157 device: &wgpu::Device,
2158 arena: &Arena,
2159 graph: &Graph,
2160 param_offsets: &HashMap<String, NodeId>,
2161 out_id: NodeId,
2162 a_id: NodeId,
2163 b_id: NodeId,
2164 b_in_arena: bool,
2165) -> (u64, u64, bool) {
2166 let max_binding = device.limits().max_storage_buffer_binding_size;
2167 if let Some((base, size)) = arena_whole_arena_bind(arena, max_binding) {
2168 return (base, size, false);
2169 }
2170 if !b_in_arena {
2171 let (base, size) = arena_window_for_nodes(device, arena, &[out_id, a_id]);
2178 return (base, size, false);
2179 }
2180 let ids = [out_id, a_id, b_id];
2181 let all_fits = arena_span_bytes(arena, &ids) <= max_binding;
2182 let b_bytes = arena.len_of(b_id) as u64;
2183 let b_is_param = tensor_is_graph_param(graph, param_offsets, b_id);
2184 let param_anchor =
2185 b_is_param && b_bytes <= max_binding && (!all_fits || b_bytes > ARENA_STAGE_CAP);
2186 let (mut base, mut size) = if param_anchor {
2187 arena_window_for_nodes(device, arena, &[b_id])
2188 } else if all_fits {
2189 arena_window_for_nodes(device, arena, &ids)
2190 } else {
2191 arena_window_for_nodes(device, arena, &[out_id])
2192 };
2193 let param_anchor = param_anchor
2194 || (b_is_param
2195 && b_bytes <= max_binding
2196 && !arena_tensor_in_window(arena, b_id, base, size));
2197 if param_anchor && !arena_tensor_in_window(arena, b_id, base, size) {
2198 (base, size) = arena_window_for_nodes(device, arena, &[b_id]);
2199 }
2200 (base, size, param_anchor)
2201}
2202
2203fn arena_expand_bind_window(
2206 arena: &Arena,
2207 ids: &[NodeId],
2208 base: &mut u64,
2209 size: &mut u64,
2210 max_binding: u64,
2211) {
2212 const ALIGN: u64 = 256;
2213 let mut lo = *base;
2214 let mut hi = base.saturating_add(*size);
2215 for &id in ids {
2216 let off = arena.offset(id) as u64;
2217 let len = arena.len_of(id) as u64;
2218 lo = lo.min(off);
2219 hi = hi.max(off.saturating_add(len));
2220 }
2221 let span = hi.saturating_sub(lo).max(1);
2222 if span > max_binding {
2223 return;
2224 }
2225 *base = (lo / ALIGN) * ALIGN;
2226 *size = span.div_ceil(ALIGN) * ALIGN;
2227 *size = (*size).max(256).min(max_binding);
2228 if (*base).saturating_add(*size) > arena.size as u64 {
2229 *base = (arena.size as u64).saturating_sub(*size);
2230 *base = (*base / ALIGN) * ALIGN;
2231 }
2232}
2233
2234fn arena_off_in_bind_window(
2235 graph: &Graph,
2236 param_offsets: &HashMap<String, NodeId>,
2237 device: &wgpu::Device,
2238 arena: &Arena,
2239 schedule: &mut Vec<Step>,
2240 scratch: &mut u64,
2241 id: NodeId,
2242 base: &mut u64,
2243 size: &mut u64,
2244) -> u32 {
2245 let max_binding = device.limits().max_storage_buffer_binding_size;
2246 if let Some((b, s)) = arena_whole_arena_bind(arena, max_binding) {
2247 *base = b;
2248 *size = s;
2249 return arena_local_off_f32(arena, id, b);
2250 }
2251 if arena_tensor_in_window(arena, id, *base, *size) {
2252 arena_local_off_f32(arena, id, *base)
2253 } else {
2254 let len = arena.len_of(id) as u64;
2255 if tensor_is_graph_param(graph, param_offsets, id) && len > max_binding {
2256 panic!(
2257 "rlx-wgpu: param node {:?} ({} bytes) exceeds max_storage_buffer_binding_size \
2258 ({max_binding}); split weights or use f16 shadow binds",
2259 id, len
2260 );
2261 }
2262 if len > ARENA_STAGE_CAP {
2263 let op = &graph.node(id).op;
2264 panic!(
2265 "rlx-wgpu: bind_window would stage {} bytes for {:?} op={op:?} \
2266 (off={}, base={}, bind_size={})",
2267 len,
2268 id,
2269 arena.offset(id),
2270 *base,
2271 *size,
2272 );
2273 }
2274 arena_off_in_window_or_stage(arena, schedule, scratch, base, size, max_binding, id)
2275 }
2276}
2277
2278fn arena_multi_op_window(
2282 dev: &wgpu::Device,
2283 arena: &Arena,
2284 graph: &Graph,
2285 param_offsets: &HashMap<String, NodeId>,
2286 _schedule: &mut Vec<Step>,
2287 scratch: &mut u64,
2288 ids: &[NodeId],
2289) -> (u64, u64, bool) {
2290 let max_binding = dev.limits().max_storage_buffer_binding_size;
2291 if let Some((base, size)) = arena_whole_arena_bind(arena, max_binding) {
2292 *scratch = arena.scratch_off as u64;
2293 return (base, size, false);
2294 }
2295 let param_anchor = if arena_span_bytes(arena, ids) > max_binding {
2296 ids.iter()
2297 .find(|&&id| {
2298 let nbytes = arena.len_of(id) as u64;
2299 tensor_is_graph_param(graph, param_offsets, id) && nbytes <= max_binding
2300 })
2301 .copied()
2302 } else {
2303 None
2304 };
2305 let mut param_anchored = param_anchor.is_some();
2306 let (mut base, mut size) = if arena_span_bytes(arena, ids) <= max_binding {
2307 arena_window_for_nodes(dev, arena, ids)
2308 } else if let Some(id) = param_anchor {
2309 arena_window_for_nodes(dev, arena, &[id])
2310 } else {
2311 arena_window_for_nodes(dev, arena, &[ids[0]])
2312 };
2313 if let Some(id) = param_anchor {
2314 if !arena_tensor_in_window(arena, id, base, size) {
2315 (base, size) = arena_window_for_nodes(dev, arena, &[id]);
2316 }
2317 param_anchored = true;
2318 } else {
2319 for &id in ids {
2320 let nbytes = arena.len_of(id) as u64;
2321 if tensor_is_graph_param(graph, param_offsets, id)
2322 && nbytes <= max_binding
2323 && !arena_tensor_in_window(arena, id, base, size)
2324 {
2325 (base, size) = arena_window_for_nodes(dev, arena, &[id]);
2326 param_anchored = true;
2327 break;
2328 }
2329 }
2330 }
2331 *scratch = arena.scratch_off as u64;
2332 if param_anchored {
2333 arena_ensure_scratch_in_window(scratch, base, size);
2334 }
2335 (base, size, param_anchored)
2336}
2337
2338fn arena_bind_window_covering_scratch_if_needed(
2339 arena: &Arena,
2340 base: u64,
2341 size: u64,
2342 scratch: u64,
2343) -> u64 {
2344 if scratch <= arena.scratch_off as u64 {
2347 return base;
2348 }
2349 if scratch >= base && scratch.saturating_add(ARENA_STAGE_CAP) <= base.saturating_add(size) {
2350 return base;
2351 }
2352 arena_window_covering_scratch(arena, base, size)
2353}
2354
2355fn arena_ensure_scratch_in_window(scratch: &mut u64, base: u64, size: u64) {
2358 let cap = ARENA_STAGE_CAP.min(size);
2359 let end = base.saturating_add(size);
2360 if *scratch < base || scratch.saturating_add(cap) > end {
2361 *scratch = end.saturating_sub(cap);
2362 *scratch = (*scratch / 256) * 256;
2363 }
2364}
2365
2366#[allow(dead_code)]
2367fn arena_off_for_window(
2368 arena: &Arena,
2369 schedule: &mut Vec<Step>,
2370 scratch: &mut u64,
2371 id: NodeId,
2372 _window_ids: &[NodeId],
2373 mut base: u64,
2374 mut size: u64,
2375 max_binding: u64,
2376 _fits_in_one_binding: bool,
2377) -> u32 {
2378 let src = arena.offset(id) as u64;
2379 let len = arena.len_of(id) as u64;
2380 if src >= base && src.saturating_add(len) <= base.saturating_add(size) {
2381 arena_local_off_f32(arena, id, base)
2382 } else {
2383 arena_off_in_window_or_stage(
2384 arena,
2385 schedule,
2386 scratch,
2387 &mut base,
2388 &mut size,
2389 max_binding,
2390 id,
2391 )
2392 }
2393}
2394
2395fn f16_shadow_bind_range(arena_base: u64, arena_size: u64, f16_buf_bytes: u64) -> (u64, u64) {
2397 const ALIGN: u64 = 256;
2398 let mut base = (arena_base / 2 / ALIGN) * ALIGN;
2399 let mut size = (arena_size / 2).div_ceil(ALIGN) * ALIGN;
2400 size = size.max(256).min(f16_buf_bytes);
2401 if base.saturating_add(size) > f16_buf_bytes {
2402 base = f16_buf_bytes.saturating_sub(size);
2403 base = (base / ALIGN) * ALIGN;
2404 }
2405 (base, size)
2406}
2407
2408fn f16_weight_bind_range(
2411 dev: &wgpu::Device,
2412 f16_buf_bytes: u64,
2413 b_off: u32,
2414 k: u32,
2415 n: u32,
2416 batch: u32,
2417 b_batch_stride: u32,
2418) -> (u64, u64, u32) {
2419 const ALIGN: u64 = 256;
2420 let max_binding = dev.limits().max_storage_buffer_binding_size;
2421 let b0 = b_off as u64;
2422 let span = (k as u64).saturating_mul(n as u64);
2423 let batch_n = batch.max(1) as u64;
2424 let stride = if batch_n > 1 {
2425 b_batch_stride as u64
2426 } else {
2427 span
2428 };
2429 let hi_elems = b0
2430 .saturating_add((batch_n - 1).saturating_mul(stride))
2431 .saturating_add(span);
2432 let lo_byte = b0.saturating_mul(2);
2433 let hi_byte = hi_elems.saturating_mul(2).saturating_add(8);
2434 let need = hi_byte.saturating_sub(lo_byte).max(1);
2435 if need > max_binding {
2436 panic!(
2437 "rlx-wgpu: f16 weight region needs {need} bytes (> {max_binding}); \
2438 matmul k={k} n={n} batch={batch}"
2439 );
2440 }
2441 let mut base = (lo_byte / ALIGN) * ALIGN;
2442 let mut size = need.div_ceil(ALIGN) * ALIGN;
2443 size = size.max(256).min(max_binding).min(f16_buf_bytes);
2444 if base.saturating_add(size) < hi_byte {
2445 base = hi_byte.saturating_sub(size);
2446 base = (base / ALIGN) * ALIGN;
2447 }
2448 if base.saturating_add(size) > f16_buf_bytes {
2449 base = f16_buf_bytes.saturating_sub(size);
2450 base = (base / ALIGN) * ALIGN;
2451 }
2452 let rebased = b_off.saturating_sub((base / 2) as u32);
2453 (base, size, rebased)
2454}
2455
2456const ARENA_STAGE_CAP: u64 = 256 * 1024 * 1024;
2457
2458const CONV2D_TILE: u32 = 4;
2461
2462fn arena_off_in_window_or_stage(
2465 arena: &Arena,
2466 schedule: &mut Vec<Step>,
2467 scratch: &mut u64,
2468 base: &mut u64,
2469 size: &mut u64,
2470 max_binding: u64,
2471 id: NodeId,
2472) -> u32 {
2473 let src = arena.offset(id) as u64;
2474 let len = arena.len_of(id) as u64;
2475 if src >= *base && src.saturating_add(len) <= (*base).saturating_add(*size) {
2476 return arena_local_off_f32(arena, id, *base);
2477 }
2478 if len > ARENA_STAGE_CAP {
2479 panic!(
2480 "rlx-wgpu: cannot stage {} bytes for node {:?} (cap {ARENA_STAGE_CAP})",
2481 len, id
2482 );
2483 }
2484 let aligned = len.div_ceil(256) * 256;
2485 let dst = *scratch;
2486 *scratch = scratch.saturating_add(aligned);
2487 schedule.push(Step::BufferCopy {
2488 src_byte_off: src,
2489 dst_byte_off: dst,
2490 bytes: len as u32,
2491 });
2492 let lo = (*base).min(dst);
2493 let hi = (*base)
2494 .saturating_add(*size)
2495 .max(dst.saturating_add(aligned));
2496 let span = hi.saturating_sub(lo).max(1);
2497 if span <= max_binding {
2498 const ALIGN: u64 = 256;
2499 *base = (lo / ALIGN) * ALIGN;
2500 *size = span.div_ceil(ALIGN) * ALIGN;
2501 *size = (*size).max(256).min(max_binding);
2502 if (*base).saturating_add(*size) > arena.size as u64 {
2503 *base = (arena.size as u64).saturating_sub(*size);
2504 *base = (*base / ALIGN) * ALIGN;
2505 }
2506 }
2507 if arena_tensor_in_window(arena, id, *base, *size) {
2508 arena_local_off_f32(arena, id, *base)
2509 } else {
2510 ((dst.saturating_sub(*base)) / 4) as u32
2511 }
2512}
2513
2514fn arena_window_covering_scratch(arena: &Arena, base: u64, size: u64) -> u64 {
2516 let scratch = arena.scratch_off as u64;
2517 if scratch >= base && scratch.saturating_add(ARENA_STAGE_CAP) <= base.saturating_add(size) {
2518 return base;
2519 }
2520 let new_base = (arena.size as u64).saturating_sub(size);
2521 (new_base / 256) * 256
2522}
2523
2524fn arena_span_bytes(arena: &Arena, ids: &[NodeId]) -> u64 {
2525 let mut lo: u64 = u64::MAX;
2526 let mut hi: u64 = 0;
2527 for &id in ids {
2528 let off = arena.offset(id) as u64;
2529 let len = arena.len_of(id) as u64;
2530 lo = lo.min(off);
2531 hi = hi.max(off.saturating_add(len));
2532 }
2533 if lo == u64::MAX {
2534 0
2535 } else {
2536 hi.saturating_sub(lo)
2537 }
2538}
2539
2540#[allow(dead_code)]
2541fn bind_two(
2542 device: &wgpu::Device,
2543 kernel: &Kernel,
2544 buf0: &wgpu::Buffer,
2545 buf1: &wgpu::Buffer,
2546) -> wgpu::BindGroup {
2547 let max_binding = device.limits().max_storage_buffer_binding_size;
2548 if buf0.size() > max_binding {
2549 panic!(
2550 "rlx-wgpu: bind_two buffer {} bytes exceeds max_storage_buffer_binding_size {}; \
2551 use bind_two_buf0_window or bind_op_output_window",
2552 buf0.size(),
2553 max_binding
2554 );
2555 }
2556 device.create_bind_group(&wgpu::BindGroupDescriptor {
2557 label: Some("rlx-wgpu bg"),
2558 layout: &kernel.bgl,
2559 entries: &[
2560 wgpu::BindGroupEntry {
2561 binding: 0,
2562 resource: buf0.as_entire_binding(),
2563 },
2564 wgpu::BindGroupEntry {
2565 binding: 1,
2566 resource: buf1.as_entire_binding(),
2567 },
2568 ],
2569 })
2570}
2571
2572fn bind_op_output_window(
2576 device: &wgpu::Device,
2577 kernel: &Kernel,
2578 arena: &Arena,
2579 out_id: NodeId,
2580 params: &wgpu::Buffer,
2581) -> wgpu::BindGroup {
2582 bind_op_window(device, kernel, arena, &[out_id], params)
2583}
2584
2585fn bind_op_window(
2586 device: &wgpu::Device,
2587 kernel: &Kernel,
2588 arena: &Arena,
2589 ids: &[NodeId],
2590 params: &wgpu::Buffer,
2591) -> wgpu::BindGroup {
2592 let max_binding = device.limits().max_storage_buffer_binding_size;
2593 let (base, size) = if arena_span_bytes(arena, ids) <= max_binding {
2594 arena_window_for_nodes(device, arena, ids)
2595 } else {
2596 arena_window_for_nodes(device, arena, &[ids[0]])
2597 };
2598 bind_two_buf0_window(device, kernel, &arena.buffer, base, size, params)
2599}
2600
2601pub(crate) fn aligned_bind_size(size: u64, base: u64, buffer_size: u64) -> Option<NonZeroU64> {
2606 let cap = (buffer_size & !3).saturating_sub(base);
2609 NonZeroU64::new(size.next_multiple_of(4).min(cap))
2610}
2611
2612fn bind_two_buf0_window(
2613 device: &wgpu::Device,
2614 kernel: &Kernel,
2615 buf0: &wgpu::Buffer,
2616 buf0_base: u64,
2617 buf0_size: u64,
2618 buf1: &wgpu::Buffer,
2619) -> wgpu::BindGroup {
2620 device.create_bind_group(&wgpu::BindGroupDescriptor {
2621 label: Some("rlx-wgpu bg window"),
2622 layout: &kernel.bgl,
2623 entries: &[
2624 wgpu::BindGroupEntry {
2625 binding: 0,
2626 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2627 buffer: buf0,
2628 offset: buf0_base,
2629 size: aligned_bind_size(buf0_size, buf0_base, buf0.size()),
2630 }),
2631 },
2632 wgpu::BindGroupEntry {
2633 binding: 1,
2634 resource: buf1.as_entire_binding(),
2635 },
2636 ],
2637 })
2638}
2639
2640fn derive_matmul_compute(
2663 dev: &wgpu::Device,
2664 graph: &Graph,
2665 mirror_acts: &HashSet<NodeId>,
2666 a_id: NodeId,
2667 b_id: NodeId,
2668 m: u32,
2669 k: u32,
2670 n: u32,
2671) -> MatmulCompute {
2672 if rlx_ir::env::flag("RLX_WGPU_MATMUL_F32_ONLY") {
2673 return MatmulCompute::F32;
2674 }
2675 use rlx_ir::DType;
2676 let a_dt = graph.node(a_id).shape.dtype();
2677 let b_dt = graph.node(b_id).shape.dtype();
2678 let any_low =
2679 matches!(a_dt, DType::F16 | DType::BF16) || matches!(b_dt, DType::F16 | DType::BF16);
2680 let coop16_aligned = m.is_multiple_of(32) && k.is_multiple_of(8) && n.is_multiple_of(32);
2690 let coop_f32_metal_aligned = k.is_multiple_of(8) && n.is_multiple_of(32);
2691 let coop_f32_portable_aligned = k.is_multiple_of(8) && n.is_multiple_of(8);
2692 let has_coop = dev
2693 .features()
2694 .contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX);
2695 let backend = crate::device::wgpu_device().map(|d| d.backend);
2696 if any_low
2701 && has_coop
2702 && dev.features().contains(wgpu::Features::SHADER_F16)
2703 && traces_to_param(graph, b_id)
2704 && coop16_aligned
2705 {
2706 return MatmulCompute::Coop16;
2707 }
2708 if !any_low && coop_f16_vk_eligible(dev, m, k, n) {
2709 if traces_to_param(graph, b_id)
2710 && !mirror_acts.contains(&a_id)
2711 && !mirror_acts.contains(&b_id)
2712 {
2713 return MatmulCompute::CoopF16Vk;
2714 }
2715 }
2716 let disabled = rlx_ir::env::flag("RLX_WGPU_NO_COOP_F32");
2725 let forced = rlx_ir::env::flag("RLX_WGPU_FORCE_COOP_F32");
2726 let metal_coop =
2733 !disabled && has_coop && coop_f32_metal_aligned && traces_to_param(graph, b_id) && forced;
2734 let _ = backend;
2735 let vulkan_coop = !disabled
2736 && has_coop
2737 && coop_f32_portable_aligned
2738 && traces_to_param(graph, b_id)
2739 && crate::device::coop_discrete_backend()
2740 && crate::device::coop_f32_8x8_supported();
2741 if metal_coop
2742 || vulkan_coop
2743 || (forced
2744 && has_coop
2745 && traces_to_param(graph, b_id)
2746 && (coop_f32_metal_aligned || coop_f32_portable_aligned))
2747 {
2748 return MatmulCompute::CoopF32;
2749 }
2750 MatmulCompute::F32
2751}
2752
2753#[allow(dead_code)]
2773fn detect_qkv_narrow_pattern(
2774 graph: &Graph,
2775 q_id: NodeId,
2776 k_id: NodeId,
2777 v_id: NodeId,
2778) -> Option<(NodeId, u32)> {
2779 let unwrap_narrow = |id: NodeId| -> Option<(NodeId, usize, usize, usize)> {
2780 let node = graph.node(id);
2781 match &node.op {
2782 Op::Narrow { axis, start, len } => Some((node.inputs[0], *axis, *start, *len)),
2783 _ => None,
2784 }
2785 };
2786 let (q_src, q_axis, q_start, q_len) = unwrap_narrow(q_id)?;
2787 let (k_src, k_axis, k_start, k_len) = unwrap_narrow(k_id)?;
2788 let (v_src, v_axis, v_start, v_len) = unwrap_narrow(v_id)?;
2789 if q_src != k_src || k_src != v_src {
2791 return None;
2792 }
2793 if q_len != k_len || k_len != v_len {
2795 return None;
2796 }
2797 if q_start != 0 || k_start != q_len || v_start != q_len * 2 {
2799 return None;
2800 }
2801 let src_rank = graph.node(q_src).shape.dims().len();
2803 if q_axis + 1 != src_rank || k_axis + 1 != src_rank || v_axis + 1 != src_rank {
2804 return None;
2805 }
2806 Some((q_src, q_len as u32))
2807}
2808
2809fn detect_residual_ln_tee_pattern(
2839 graph: &Graph,
2840) -> (
2841 HashMap<NodeId, (NodeId, NodeId, NodeId, NodeId, NodeId)>,
2842 HashSet<NodeId>,
2843) {
2844 use rlx_ir::op::BinaryOp;
2845 let mut consumers: HashMap<NodeId, usize> = HashMap::new();
2847 for node in graph.nodes() {
2848 for &input in &node.inputs {
2849 *consumers.entry(input).or_insert(0) += 1;
2850 }
2851 }
2852 for &out in &graph.outputs {
2853 *consumers.entry(out).or_insert(0) += 1;
2854 }
2855
2856 let mut ln_to_tee = HashMap::new();
2857 let mut skip_adds = HashSet::new();
2858 for node in graph.nodes() {
2859 let Op::LayerNorm { axis: _, eps: _ } = &node.op else {
2860 continue;
2861 };
2862 if node.inputs.len() < 3 {
2863 continue;
2864 } let in_id = node.inputs[0];
2866 let in_node = graph.node(in_id);
2867 if !matches!(in_node.op, Op::Binary(BinaryOp::Add)) {
2868 continue;
2869 }
2870 if consumers.get(&in_id).copied().unwrap_or(0) < 2 {
2873 continue;
2874 }
2875 if in_node.inputs.len() != 2 {
2878 continue;
2879 }
2880 let h_id = in_node.inputs[0];
2881 let delta_id = in_node.inputs[1];
2882 if graph.node(h_id).shape.dims() != node.shape.dims() {
2883 continue;
2884 }
2885 if graph.node(delta_id).shape.dims() != node.shape.dims() {
2886 continue;
2887 }
2888 let gamma_id = node.inputs[1];
2889 let beta_id = node.inputs[2];
2890 ln_to_tee.insert(node.id, (h_id, delta_id, gamma_id, beta_id, in_id));
2891 skip_adds.insert(in_id);
2892 }
2893 (ln_to_tee, skip_adds)
2894}
2895
2896fn detect_split_qkv_pattern(graph: &Graph) -> HashMap<NodeId, (NodeId, NodeId, NodeId)> {
2897 let mut consumers: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
2899 for node in graph.nodes() {
2900 for &input in &node.inputs {
2901 consumers.entry(input).or_default().push(node.id);
2902 }
2903 }
2904 for &out_id in &graph.outputs {
2907 consumers.entry(out_id).or_default().push(NodeId(u32::MAX));
2908 }
2909
2910 let mut result = HashMap::new();
2911 for node in graph.nodes() {
2912 if !matches!(node.op, Op::FusedMatMulBiasAct { activation: None }) {
2913 continue;
2914 }
2915 let cs = match consumers.get(&node.id) {
2916 Some(c) if c.len() == 3 => c,
2917 _ => continue,
2918 };
2919 let dims = node.shape.dims();
2920 if dims.is_empty() {
2921 continue;
2922 }
2923 let last_axis = dims.len() - 1;
2924 let n = dims[last_axis].unwrap_static();
2925 if n % 3 != 0 {
2926 continue;
2927 }
2928 let head_width = n / 3;
2929
2930 let mut narrows: Vec<(usize, NodeId)> = Vec::with_capacity(3);
2932 let mut all_match = true;
2933 for &c in cs {
2934 let cn = graph.node(c);
2935 match cn.op {
2936 Op::Narrow { axis, start, len }
2937 if axis == last_axis && len == head_width && cn.inputs[0] == node.id =>
2938 {
2939 narrows.push((start, c));
2940 }
2941 _ => {
2942 all_match = false;
2943 break;
2944 }
2945 }
2946 }
2947 if !all_match {
2948 continue;
2949 }
2950 narrows.sort_by_key(|&(start, _)| start);
2951 if narrows[0].0 != 0 || narrows[1].0 != head_width || narrows[2].0 != 2 * head_width {
2952 continue;
2953 }
2954 result.insert(node.id, (narrows[0].1, narrows[1].1, narrows[2].1));
2955 }
2956 result
2957}
2958
2959fn node_is_arena_param(param_offsets: &HashMap<String, NodeId>, id: NodeId) -> bool {
2965 param_offsets.values().any(|&nid| nid == id)
2966}
2967
2968fn traces_to_param(graph: &Graph, mut id: NodeId) -> bool {
2969 loop {
2970 let node = graph.node(id);
2971 match &node.op {
2972 Op::Param { .. } => return true,
2973 Op::Cast { .. } | Op::Reshape { .. } | Op::Transpose { .. } => {
2974 if node.inputs.is_empty() {
2975 return false;
2976 }
2977 id = node.inputs[0];
2978 }
2979 _ => return false,
2980 }
2981 }
2982}
2983
2984fn tensor_is_graph_param(
2985 graph: &Graph,
2986 param_offsets: &HashMap<String, NodeId>,
2987 id: NodeId,
2988) -> bool {
2989 node_is_arena_param(param_offsets, id) || traces_to_param(graph, id)
2990}
2991
2992fn traces_to_input(graph: &Graph, mut id: NodeId) -> bool {
2993 loop {
2994 let node = graph.node(id);
2995 match &node.op {
2996 Op::Input { .. } => return true,
2997 Op::Cast { .. } | Op::Reshape { .. } => {
2998 if node.inputs.is_empty() {
2999 return false;
3000 }
3001 id = node.inputs[0];
3002 }
3003 _ => return false,
3004 }
3005 }
3006}
3007
3008fn schedule_uses_coop_f16_vk(schedule: &[Step]) -> bool {
3011 schedule.iter().any(|s| {
3012 matches!(
3013 s,
3014 Step::Matmul {
3015 compute_precision: MatmulCompute::CoopF16Vk,
3016 ..
3017 } | Step::MatmulQkv {
3018 kind: MatmulQkvKind::CoopF16Vk,
3019 ..
3020 }
3021 )
3022 })
3023}
3024
3025fn register_coop_f16_vk_b_param(
3026 map: &mut HashMap<u32, String>,
3027 param_offsets: &HashMap<String, NodeId>,
3028 b_id: NodeId,
3029 b_off_f32: u32,
3030 compute: MatmulCompute,
3031) {
3032 if compute != MatmulCompute::CoopF16Vk {
3033 return;
3034 }
3035 for (name, &id) in param_offsets {
3036 if id == b_id {
3037 map.insert(b_off_f32, name.clone());
3038 return;
3039 }
3040 }
3041}
3042
3043fn tensor_host_name(
3044 input_offsets: &HashMap<String, NodeId>,
3045 param_offsets: &HashMap<String, NodeId>,
3046 id: NodeId,
3047) -> String {
3048 for (name, &nid) in input_offsets {
3049 if nid == id {
3050 return name.clone();
3051 }
3052 }
3053 for (name, &nid) in param_offsets {
3054 if nid == id {
3055 return name.clone();
3056 }
3057 }
3058 panic!("rlx-wgpu: CoopF16Vk host activation source {id} is not an input or param");
3059}
3060
3061fn host_tensor_f32<'a>(
3062 name: &str,
3063 inputs: &'a [(&str, &[f32])],
3064 stashed_params: &'a HashMap<String, Vec<f32>>,
3065) -> Option<&'a [f32]> {
3066 inputs
3067 .iter()
3068 .find(|(n, _)| *n == name)
3069 .map(|(_, d)| *d)
3070 .or_else(|| stashed_params.get(name).map(|v| v.as_slice()))
3071}
3072
3073fn apply_activation_host(act: Activation, data: &[f32]) -> Vec<f32> {
3074 data.iter()
3075 .map(|&x| match act {
3076 Activation::Relu => x.max(0.0),
3077 Activation::Sigmoid => 1.0 / (1.0 + (-x).exp()),
3078 Activation::Tanh => x.tanh(),
3079 Activation::Exp => x.exp(),
3080 Activation::Log => x.ln(),
3081 Activation::Sqrt => x.sqrt(),
3082 Activation::Rsqrt => 1.0 / x.sqrt(),
3083 Activation::Neg => -x,
3084 Activation::Abs => x.abs(),
3085 Activation::Gelu | Activation::GeluApprox => {
3086 let c = 0.797_884_6_f32;
3087 let x3 = x * x * x;
3088 let inner = (c * (x + 0.044_715 * x3)).clamp(-15.0, 15.0);
3089 0.5 * x * (1.0 + inner.tanh())
3090 }
3091 Activation::Silu => {
3092 let nx = (-x).clamp(-88.0, 88.0);
3093 x / (1.0 + nx.exp())
3094 }
3095 Activation::Round => x.round(),
3096 Activation::Sin => x.sin(),
3097 Activation::Cos => x.cos(),
3098 Activation::Tan => x.tan(),
3099 Activation::Atan => x.atan(),
3100 })
3101 .collect()
3102}
3103
3104fn collect_coop_f16_vk_mirror_activations(graph: &Graph, dev: &wgpu::Device) -> HashSet<NodeId> {
3106 let mut acts = HashSet::new();
3107 for node in graph.nodes() {
3108 if !matches!(node.op, Op::MatMul) {
3109 continue;
3110 }
3111 let a_id = node.inputs[0];
3112 let b_id = node.inputs[1];
3113 let a_shape = graph.node(a_id).shape.dims();
3114 let b_shape = graph.node(b_id).shape.dims();
3115 if a_shape.len() != 2 || b_shape.len() != 2 {
3116 continue;
3117 }
3118 let m = a_shape[0].unwrap_static() as u32;
3119 let k = a_shape[1].unwrap_static() as u32;
3120 let n = b_shape[1].unwrap_static() as u32;
3121 if !coop_f16_vk_eligible(dev, m, k, n) || !traces_to_param(graph, b_id) {
3122 continue;
3123 }
3124 if matches!(graph.node(a_id).op, Op::Activation(_)) {
3125 acts.insert(a_id);
3126 }
3127 if matches!(graph.node(b_id).op, Op::Activation(_)) {
3128 acts.insert(b_id);
3129 }
3130 }
3131 acts
3132}
3133
3134fn maybe_push_coop_f16_vk_casts(
3137 graph: &Graph,
3138 a_id: NodeId,
3139 b_id: NodeId,
3140 mirror_acts: &HashSet<NodeId>,
3141 device: &wgpu::Device,
3142 arena: &Arena,
3143 schedule: &mut Vec<Step>,
3144 uniforms: &mut Vec<wgpu::Buffer>,
3145 bind_groups: &mut Vec<wgpu::BindGroup>,
3146 mm_cast: &Option<&'static Kernel>,
3147 compute_precision: MatmulCompute,
3148 a_off_f32: u32,
3149 m: u32,
3150 k: u32,
3151 batch: u32,
3152 b_off_f32: u32,
3153 n: u32,
3154) {
3155 if compute_precision != MatmulCompute::CoopF16Vk {
3156 return;
3157 }
3158 let batch_n = batch.max(1);
3159 if !traces_to_input(graph, a_id)
3160 && !traces_to_param(graph, a_id)
3161 && !mirror_acts.contains(&a_id)
3162 {
3163 let a_elems = m.saturating_mul(k).saturating_mul(batch_n);
3164 let (base, size) = arena_window_for_nodes(device, arena, &[a_id]);
3165 push_cast_f32_to_f16_step(
3166 device,
3167 arena,
3168 base,
3169 size,
3170 schedule,
3171 uniforms,
3172 bind_groups,
3173 mm_cast,
3174 a_off_f32,
3175 a_elems,
3176 );
3177 }
3178 if !traces_to_input(graph, b_id)
3179 && !traces_to_param(graph, b_id)
3180 && !mirror_acts.contains(&b_id)
3181 {
3182 let b_elems = k.saturating_mul(n).saturating_mul(batch_n);
3183 let (base, size) = arena_window_for_nodes(device, arena, &[b_id]);
3184 push_cast_f32_to_f16_step(
3185 device,
3186 arena,
3187 base,
3188 size,
3189 schedule,
3190 uniforms,
3191 bind_groups,
3192 mm_cast,
3193 b_off_f32,
3194 b_elems,
3195 );
3196 }
3197}
3198
3199fn build_matmul_qkv_coop_f16_vk_bind_group(
3200 device: &wgpu::Device,
3201 mqk: &Kernel,
3202 arena: &Arena,
3203 arena_base: u64,
3204 arena_size: u64,
3205 params: &wgpu::Buffer,
3206 k: u32,
3207 n: u32,
3208 b_off: u32,
3209) -> (wgpu::BindGroup, u32) {
3210 let f16_buf = arena
3211 .f16_buffer
3212 .as_ref()
3213 .expect("CoopF16Vk QKV requires SHADER_F16 f16 shadow arena");
3214 let (f16_res, rebased_b) = {
3215 let (base, size, rebased) =
3216 f16_weight_bind_range(device, f16_buf.size(), b_off, k, n, 1, 0);
3217 (
3218 wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3219 buffer: f16_buf,
3220 offset: base,
3221 size: NonZeroU64::new(size),
3222 }),
3223 rebased,
3224 )
3225 };
3226 (
3227 device.create_bind_group(&wgpu::BindGroupDescriptor {
3228 label: Some("rlx-wgpu matmul_qkv_coop_f16_vk bg"),
3229 layout: &mqk.bgl,
3230 entries: &[
3231 wgpu::BindGroupEntry {
3232 binding: 0,
3233 resource: f16_res,
3234 },
3235 wgpu::BindGroupEntry {
3236 binding: 1,
3237 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3238 buffer: &arena.buffer,
3239 offset: arena_base,
3240 size: NonZeroU64::new(arena_size),
3241 }),
3242 },
3243 wgpu::BindGroupEntry {
3244 binding: 2,
3245 resource: params.as_entire_binding(),
3246 },
3247 ],
3248 }),
3249 rebased_b,
3250 )
3251}
3252fn push_cast_f32_to_f16_step(
3256 device: &wgpu::Device,
3257 arena: &Arena,
3258 arena_base: u64,
3259 arena_size: u64,
3260 schedule: &mut Vec<Step>,
3261 uniforms: &mut Vec<wgpu::Buffer>,
3262 bind_groups: &mut Vec<wgpu::BindGroup>,
3263 mm_cast: &Option<&'static Kernel>,
3264 src_off: u32,
3265 len: u32,
3266) {
3267 let kernel = match mm_cast {
3268 Some(k) => *k,
3269 None => return, };
3271 let f16_buf = match &arena.f16_buffer {
3272 Some(b) => b,
3273 None => return,
3274 };
3275 let p = CastF32ToF16Params {
3276 src_off: src_off.saturating_sub((arena_base / 4) as u32),
3277 len,
3278 _p0: 0,
3279 _p1: 0,
3280 };
3281 let u = device.create_buffer(&wgpu::BufferDescriptor {
3282 label: Some("rlx-wgpu cast_f32_to_f16 uniform"),
3283 size: std::mem::size_of::<CastF32ToF16Params>() as u64,
3284 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3285 mapped_at_creation: false,
3286 });
3287 let dev = wgpu_device().expect("rlx-wgpu: device gone");
3289 dev.queue.write_buffer(&u, 0, bytemuck::bytes_of(&p));
3290 let (f16_base, f16_size) = f16_shadow_bind_range(arena_base, arena_size, f16_buf.size());
3291 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
3292 label: Some("rlx-wgpu cast_f32_to_f16 bg"),
3293 layout: &kernel.bgl,
3294 entries: &[
3295 wgpu::BindGroupEntry {
3296 binding: 0,
3297 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3298 buffer: f16_buf,
3299 offset: f16_base,
3300 size: NonZeroU64::new(f16_size),
3301 }),
3302 },
3303 wgpu::BindGroupEntry {
3304 binding: 1,
3305 resource: u.as_entire_binding(),
3306 },
3307 wgpu::BindGroupEntry {
3308 binding: 2,
3309 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3310 buffer: &arena.buffer,
3311 offset: arena_base,
3312 size: NonZeroU64::new(arena_size),
3313 }),
3314 },
3315 ],
3316 });
3317 schedule.push(Step::CastF32ToF16 { params: p });
3318 uniforms.push(u);
3319 bind_groups.push(bg);
3320}
3321
3322fn build_matmul_bind_group(
3326 device: &wgpu::Device,
3327 mm_k: &Kernel,
3328 _mm_w: &Kernel,
3329 mm_f16w: &Option<&'static Kernel>,
3330 mm_f16c: &Option<&'static Kernel>,
3331 mm_coop: &Option<&'static Kernel>,
3332 mm_coop_f32: &Option<&'static Kernel>,
3333 arena: &Arena,
3334 arena_base: u64,
3335 arena_size: u64,
3336 params: &wgpu::Buffer,
3337 b_is_param: bool,
3338 compute_precision: MatmulCompute,
3339 k: u32,
3340 n: u32,
3341 batch: u32,
3342 b_off: u32,
3343 b_batch_stride: u32,
3344) -> (wgpu::BindGroup, u32) {
3345 let f16_bind = |b_off: u32| -> (wgpu::BindingResource<'_>, u32) {
3346 let f16_buf = arena
3347 .f16_buffer
3348 .as_ref()
3349 .expect("f16 weight bind without f16_buffer");
3350 let (base, size, rebased) =
3351 f16_weight_bind_range(device, f16_buf.size(), b_off, k, n, batch, b_batch_stride);
3352 (
3353 wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3354 buffer: f16_buf,
3355 offset: base,
3356 size: NonZeroU64::new(size),
3357 }),
3358 rebased,
3359 )
3360 };
3361 if compute_precision == MatmulCompute::CoopF16Vk
3362 && let (Some(coop_vk), Some(_f16_buf)) =
3363 (matmul_coop_f16_vulkan_kernel(device), &arena.f16_buffer)
3364 {
3365 let (f16_res, rebased_b) = f16_bind(b_off);
3366 return (
3367 device.create_bind_group(&wgpu::BindGroupDescriptor {
3368 label: Some("rlx-wgpu matmul_coop_f16_vulkan bg"),
3369 layout: &coop_vk.bgl,
3370 entries: &[
3371 wgpu::BindGroupEntry {
3372 binding: 0,
3373 resource: f16_res,
3374 },
3375 wgpu::BindGroupEntry {
3376 binding: 1,
3377 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3378 buffer: &arena.buffer,
3379 offset: arena_base,
3380 size: NonZeroU64::new(arena_size),
3381 }),
3382 },
3383 wgpu::BindGroupEntry {
3384 binding: 2,
3385 resource: params.as_entire_binding(),
3386 },
3387 ],
3388 }),
3389 rebased_b,
3390 );
3391 }
3392 if b_is_param
3393 && compute_precision == MatmulCompute::CoopF32
3394 && let Some(coop_f32) = mm_coop_f32
3395 {
3396 return (
3399 device.create_bind_group(&wgpu::BindGroupDescriptor {
3400 label: Some("rlx-wgpu matmul_coop_f32 bg"),
3401 layout: &coop_f32.bgl,
3402 entries: &[
3403 wgpu::BindGroupEntry {
3404 binding: 0,
3405 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3406 buffer: &arena.buffer,
3407 offset: arena_base,
3408 size: NonZeroU64::new(arena_size),
3409 }),
3410 },
3411 wgpu::BindGroupEntry {
3412 binding: 1,
3413 resource: params.as_entire_binding(),
3414 },
3415 ],
3416 }),
3417 b_off,
3418 );
3419 }
3420 if b_is_param
3421 && compute_precision == MatmulCompute::Coop16
3422 && let (Some(_f16_buf), Some(coop)) = (&arena.f16_buffer, mm_coop)
3423 {
3424 let (f16_res, rebased_b) = f16_bind(b_off);
3425 return (
3429 device.create_bind_group(&wgpu::BindGroupDescriptor {
3430 label: Some("rlx-wgpu matmul_coop16 bg"),
3431 layout: &coop.bgl,
3432 entries: &[
3433 wgpu::BindGroupEntry {
3434 binding: 0,
3435 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3436 buffer: &arena.buffer,
3437 offset: arena_base,
3438 size: NonZeroU64::new(arena_size),
3439 }),
3440 },
3441 wgpu::BindGroupEntry {
3442 binding: 1,
3443 resource: params.as_entire_binding(),
3444 },
3445 wgpu::BindGroupEntry {
3446 binding: 2,
3447 resource: f16_res,
3448 }, ],
3450 }),
3451 rebased_b,
3452 );
3453 }
3454 if b_is_param
3455 && compute_precision == MatmulCompute::F16
3456 && let (Some(_f16_buf), Some(f16c)) = (&arena.f16_buffer, mm_f16c)
3457 {
3458 let (f16_res, rebased_b) = f16_bind(b_off);
3459 return (
3460 device.create_bind_group(&wgpu::BindGroupDescriptor {
3461 label: Some("rlx-wgpu matmul_f16_compute bg"),
3462 layout: &f16c.bgl,
3463 entries: &[
3464 wgpu::BindGroupEntry {
3465 binding: 0,
3466 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3467 buffer: &arena.buffer,
3468 offset: arena_base,
3469 size: NonZeroU64::new(arena_size),
3470 }),
3471 },
3472 wgpu::BindGroupEntry {
3473 binding: 1,
3474 resource: params.as_entire_binding(),
3475 },
3476 wgpu::BindGroupEntry {
3477 binding: 2,
3478 resource: f16_res,
3479 },
3480 ],
3481 }),
3482 rebased_b,
3483 );
3484 }
3485 let f16w_opt_in = rlx_ir::env::flag("RLX_WGPU_F16_WEIGHTS");
3486 if b_is_param
3487 && f16w_opt_in
3488 && let (Some(_f16_buf), Some(f16w)) = (&arena.f16_buffer, mm_f16w)
3489 {
3490 let (f16_res, rebased_b) = f16_bind(b_off);
3491 return (
3492 device.create_bind_group(&wgpu::BindGroupDescriptor {
3493 label: Some("rlx-wgpu matmul_f16w bg"),
3494 layout: &f16w.bgl,
3495 entries: &[
3496 wgpu::BindGroupEntry {
3497 binding: 0,
3498 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3499 buffer: &arena.buffer,
3500 offset: arena_base,
3501 size: NonZeroU64::new(arena_size),
3502 }),
3503 },
3504 wgpu::BindGroupEntry {
3505 binding: 1,
3506 resource: params.as_entire_binding(),
3507 },
3508 wgpu::BindGroupEntry {
3509 binding: 2,
3510 resource: f16_res,
3511 },
3512 ],
3513 }),
3514 rebased_b,
3515 );
3516 }
3517 (
3518 bind_two_buf0_window(device, mm_k, &arena.buffer, arena_base, arena_size, params),
3519 b_off,
3520 )
3521}