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, Kernel, LayerNormBwdParams, LayerNormParams,
38 Mamba2Params, MatmulQkvParams, NarrowConcatParams, Pool1dParams, Pool2dParams, Pool3dParams,
39 ReduceParams, RmsNormBwdParams, RnnParams, RopeBwdParams, RopeParams, SampleParams,
40 ScatterAddParams, SceParams, SelectiveScanParams, SoftmaxParams, TopKParams, TransposeParams,
41 UmapKnnParams, UnaryParams, WelchPeaksGpuParams, WhereParams, gather_split_kernel,
42 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
98fn hash_f32_input(data: &[f32]) -> u64 {
101 let bytes = bytemuck::cast_slice(data);
102 let mut h: u64 = 0xcbf29ce484222325;
103 h ^= data.len() as u64;
104 h = h.wrapping_mul(0x100000001b3);
105 for chunk in bytes.chunks(8) {
106 let mut arr = [0u8; 8];
107 arr[..chunk.len()].copy_from_slice(chunk);
108 h ^= u64::from_le_bytes(arr);
109 h = h.wrapping_mul(0x100000001b3);
110 }
111 h
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125enum MatmulCompute {
126 F32,
127 F16,
128 Coop16,
129 CoopF32,
134 CoopF16Vk,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141enum MatmulQkvKind {
142 F32,
143 CoopF32,
144 CoopF16Vk,
145}
146
147#[allow(dead_code)]
157#[derive(Debug, Clone, Copy)]
158struct CastF32ToF16Params {
159 pub src_off: u32, pub len: u32,
161 pub _p0: u32,
162 pub _p1: u32,
163}
164unsafe impl bytemuck::Pod for CastF32ToF16Params {}
165unsafe impl bytemuck::Zeroable for CastF32ToF16Params {}
166
167#[allow(dead_code)]
176enum Step {
177 CastF32ToF16 {
178 params: CastF32ToF16Params,
179 },
180 Matmul {
181 m: u32,
182 k: u32,
183 n: u32,
184 a_off_f32: u32,
185 b_off_f32: u32,
186 c_off_f32: u32,
187 batch: u32,
188 a_batch_stride: u32,
189 b_batch_stride: u32,
190 c_batch_stride: u32,
191 has_bias: u32,
192 bias_off_f32: u32,
193 act_id: u32, b_is_param: bool,
200 compute_precision: MatmulCompute,
206 },
207 Binary {
208 params: BinaryParams,
209 },
210 Compare {
211 params: BinaryParams,
212 },
213 Unary {
214 params: UnaryParams,
215 f16_mirror: bool,
216 },
217 Where {
218 params: WhereParams,
219 },
220 Fma {
221 params: FmaParams,
222 },
223 Reduce {
224 params: ReduceParams,
225 },
226 Softmax {
227 params: SoftmaxParams,
228 },
229 SoftmaxCrossEntropy {
230 params: SceParams,
231 },
232 LayerNorm {
233 params: LayerNormParams,
234 },
235 Cumsum {
236 params: CumsumParams,
237 },
238 FftGpu {
240 src_off: u32,
241 dst_off: u32,
242 outer: u32,
243 n: u32,
244 inverse: u32,
245 norm_scale: f32,
246 },
247 FftHost {
250 src_byte_off: u32,
251 dst_byte_off: u32,
252 outer: u32,
253 n_complex: u32,
254 inverse: bool,
255 norm_tag: u32,
256 dtype_tag: u32,
257 },
258 ScanHost {
261 plan: std::sync::Arc<rlx_cpu::thunk::ScanBodyPlan>,
262 outer_init_off: usize,
263 outer_final_off: usize,
264 length: u32,
265 save_trajectory: bool,
266 xs_outer: Vec<(usize, usize)>,
267 bcast_outer: Vec<(usize, usize)>,
268 },
269 WelchPeaksHost {
271 spec_byte_off: u32,
272 dst_byte_off: u32,
273 welch_batch: u32,
274 n_fft: u32,
275 n_segments: u32,
276 k: u32,
277 },
278 LogMelHost {
279 spec_byte_off: u32,
280 filt_byte_off: u32,
281 dst_byte_off: u32,
282 outer: u32,
283 n_fft: u32,
284 n_bins: u32,
285 n_mels: u32,
286 },
287 LogMelBackwardHost {
288 spec_byte_off: u32,
289 filt_byte_off: u32,
290 dy_byte_off: u32,
291 dst_byte_off: u32,
292 outer: u32,
293 n_fft: u32,
294 n_bins: u32,
295 n_mels: u32,
296 },
297 Im2ColHost {
299 x_byte_off: u32,
300 col_byte_off: u32,
301 n: u32,
302 c_in: u32,
303 h: u32,
304 w: u32,
305 h_out: u32,
306 w_out: u32,
307 kh: u32,
308 kw: u32,
309 sh: u32,
310 sw: u32,
311 ph: u32,
312 pw: u32,
313 dh: u32,
314 dw_dil: u32,
315 },
316 RngNormalHost {
318 dst_byte_off: u32,
319 len: u32,
320 mean: f32,
321 scale: f32,
322 key: u64,
323 op_seed: Option<f32>,
324 },
325 RngUniformHost {
327 dst_byte_off: u32,
328 len: u32,
329 low: f32,
330 high: f32,
331 key: u64,
332 op_seed: Option<f32>,
333 },
334 BufferCopy {
338 src_byte_off: u64,
341 dst_byte_off: u64,
342 bytes: u32,
343 },
344 Copy {
345 params: CopyParams,
346 },
347 ElementwiseRegion {
353 params: ElementwiseRegionParams,
354 },
355 BatchElementwiseRegion {
356 params: BatchElementwiseRegionParams,
357 },
358 Transpose {
359 params: TransposeParams,
360 meta_idx: usize,
361 },
362 Narrow {
363 params: NarrowConcatParams,
364 },
365 Concat {
366 params: NarrowConcatParams,
367 }, Gather {
369 params: GatherParams,
370 },
371 GatherAxis {
372 params: GatherAxisParams,
373 },
374 Attention {
375 params: AttentionParams,
376 mask_buf: Option<wgpu::Buffer>,
377 },
378 AttentionBackward {
379 params: AttentionBwdParams,
380 mask_buf: Option<wgpu::Buffer>,
381 },
382 Rope {
383 params: RopeParams,
384 },
385 Expand {
386 params: ExpandParams,
387 meta_idx: usize,
388 },
389 Argmax {
390 params: ArgmaxParams,
391 },
392 Pool2d {
393 params: Pool2dParams,
394 },
395 Conv2d {
396 params: Conv2dParams,
397 },
398 Pool1d {
399 params: Pool1dParams,
400 },
401 Pool3d {
402 params: Pool3dParams,
403 },
404 Conv1d {
405 params: Conv1dParams,
406 },
407 Conv3d {
408 params: Conv3dParams,
409 },
410 ScatterAdd {
411 params: ScatterAddParams,
412 },
413 TopK {
414 params: TopKParams,
415 },
416 WelchPeaksGpu {
417 params: WelchPeaksGpuParams,
418 },
419 GroupedMatmul {
420 params: GroupedMatmulParams,
421 },
422 Sample {
423 params: SampleParams,
424 },
425 SelectiveScan {
426 params: SelectiveScanParams,
427 },
428 Mamba2 {
430 params: Mamba2Params,
431 },
432 Gru {
434 params: GruParams,
435 },
436 Rnn {
438 params: RnnParams,
439 },
440 GruHost {
442 x: u32,
443 w_ih: u32,
444 w_hh: u32,
445 b_ih: u32,
446 b_hh: u32,
447 h0: u32,
448 dst: u32,
449 batch: u32,
450 seq: u32,
451 input_size: u32,
452 hidden: u32,
453 num_layers: u32,
454 bidirectional: bool,
455 carry: bool,
456 },
457 RnnHost {
459 x: u32,
460 w_ih: u32,
461 w_hh: u32,
462 bias: u32,
463 h0: u32,
464 dst: u32,
465 batch: u32,
466 seq: u32,
467 input_size: u32,
468 hidden: u32,
469 num_layers: u32,
470 bidirectional: bool,
471 carry: bool,
472 relu: bool,
473 },
474 DequantMatmul {
475 params: DequantMatmulParams,
476 },
477 GatherSplit {
483 n_out: u32,
484 n_idx: u32,
485 dim: u32,
486 vocab: u32,
487 table_byte_off: u64,
488 idx_byte_off: u64,
489 out_byte_off: u64,
490 },
491 DequantMatmulGguf {
493 m: u32,
494 k: u32,
495 n: u32,
496 scheme_id: u32,
497 x_byte_off: u64,
501 w_byte_off: u64,
502 out_byte_off: u64,
503 },
504 DequantGroupedMatmulGguf {
506 m: u32,
507 k: u32,
508 n: u32,
509 num_experts: u32,
510 scheme_id: u32,
511 x_byte_off: u64,
512 w_byte_off: u64,
513 idx_byte_off: u64,
514 out_byte_off: u64,
515 },
516 GatedDeltaNet {
518 q_byte_off: u32,
519 k_byte_off: u32,
520 v_byte_off: u32,
521 g_byte_off: u32,
522 beta_byte_off: u32,
523 state_byte_off: u32,
524 dst_byte_off: u32,
525 batch: u32,
526 seq: u32,
527 heads: u32,
528 state_size: u32,
529 use_carry: bool,
530 },
531 Lstm {
532 x_byte_off: u32,
533 w_ih_byte_off: u32,
534 w_hh_byte_off: u32,
535 bias_byte_off: u32,
536 h0_byte_off: u32,
537 c0_byte_off: u32,
538 dst_byte_off: u32,
539 batch: u32,
540 seq: u32,
541 input_size: u32,
542 hidden: u32,
543 num_layers: u32,
544 bidirectional: bool,
545 carry: bool,
546 },
547 ConvTranspose2d {
548 src_byte_off: u32,
549 weight_byte_off: u32,
550 dst_byte_off: u32,
551 n: u32,
552 c_in: u32,
553 h: u32,
554 w_in: u32,
555 c_out: u32,
556 h_out: u32,
557 w_out: u32,
558 kh: u32,
559 kw: u32,
560 sh: u32,
561 sw: u32,
562 ph: u32,
563 pw: u32,
564 dh: u32,
565 dw: u32,
566 groups: u32,
567 },
568 GroupNormHost {
570 src_byte_off: u32,
571 gamma_byte_off: u32,
572 beta_byte_off: u32,
573 dst_byte_off: u32,
574 n: u32,
575 c: u32,
576 h: u32,
577 w: u32,
578 num_groups: u32,
579 eps: f32,
580 },
581 LayerNorm2dHost {
583 src_byte_off: u32,
584 gamma_byte_off: u32,
585 beta_byte_off: u32,
586 dst_byte_off: u32,
587 n: u32,
588 c: u32,
589 h: u32,
590 w: u32,
591 eps: f32,
592 },
593 ResizeNearest2xHost {
595 src_byte_off: u32,
596 dst_byte_off: u32,
597 n: u32,
598 c: u32,
599 h: u32,
600 w: u32,
601 },
602 ReverseHost {
604 src_byte_off: u32,
605 dst_byte_off: u32,
606 dims: Vec<u32>,
607 rev_mask: Vec<bool>,
608 elem_bytes: u32,
609 },
610 ArgReduceHost {
612 src_byte_off: u32,
613 dst_byte_off: u32,
614 outer: u32,
615 reduced: u32,
616 inner: u32,
617 is_max: bool,
618 },
619 Llada2GroupLimitedGate {
620 sig_byte_off: u32,
621 route_byte_off: u32,
622 out_byte_off: u32,
623 n_elems: u32,
624 attrs: [u8; 20],
625 },
626 UmapKnn {
627 params: UmapKnnParams,
628 },
629 UmapKnnHost {
631 pairwise_byte_off: u32,
632 out_byte_off: u32,
633 n: u32,
634 k: u32,
635 },
636 MsDeformAttnHost {
638 in_offs: Vec<(u32, u32)>, out_byte_off: u32,
640 out_bytes: u32,
641 attrs: Vec<u8>,
642 },
643 #[cfg(feature = "splat")]
645 GaussianSplatRender {
646 positions_byte_off: u32,
647 positions_len: u32,
648 scales_byte_off: u32,
649 scales_len: u32,
650 rotations_byte_off: u32,
651 rotations_len: u32,
652 opacities_byte_off: u32,
653 opacities_len: u32,
654 colors_byte_off: u32,
655 colors_len: u32,
656 sh_coeffs_byte_off: u32,
657 sh_coeffs_len: u32,
658 meta_byte_off: u32,
659 dst_byte_off: u32,
660 dst_len: u32,
661 width: u32,
662 height: u32,
663 tile_size: u32,
664 radius_scale: f32,
665 alpha_cutoff: f32,
666 max_splat_steps: u32,
667 transmittance_threshold: f32,
668 max_list_entries: u32,
669 },
670 #[cfg(feature = "splat")]
672 GaussianSplatRenderBackward {
673 positions_byte_off: u32,
674 positions_len: u32,
675 scales_byte_off: u32,
676 scales_len: u32,
677 rotations_byte_off: u32,
678 rotations_len: u32,
679 opacities_byte_off: u32,
680 opacities_len: u32,
681 colors_byte_off: u32,
682 colors_len: u32,
683 sh_coeffs_byte_off: u32,
684 sh_coeffs_len: u32,
685 meta_byte_off: u32,
686 d_loss_byte_off: u32,
687 d_loss_len: u32,
688 packed_byte_off: u32,
689 packed_len: u32,
690 width: u32,
691 height: u32,
692 tile_size: u32,
693 radius_scale: f32,
694 alpha_cutoff: f32,
695 max_splat_steps: u32,
696 transmittance_threshold: f32,
697 max_list_entries: u32,
698 loss_grad_clip: f32,
699 sh_band: u32,
700 max_anisotropy: f32,
701 },
702 #[cfg(feature = "splat")]
703 GaussianSplatPrepare {
704 positions_byte_off: u32,
705 positions_len: u32,
706 scales_byte_off: u32,
707 scales_len: u32,
708 rotations_byte_off: u32,
709 rotations_len: u32,
710 opacities_byte_off: u32,
711 opacities_len: u32,
712 colors_byte_off: u32,
713 colors_len: u32,
714 sh_coeffs_byte_off: u32,
715 sh_coeffs_len: u32,
716 meta_byte_off: u32,
717 meta_len: u32,
718 prep_byte_off: u32,
719 prep_len: u32,
720 width: u32,
721 height: u32,
722 tile_size: u32,
723 radius_scale: f32,
724 alpha_cutoff: f32,
725 max_splat_steps: u32,
726 transmittance_threshold: f32,
727 max_list_entries: u32,
728 },
729 #[cfg(feature = "splat")]
730 GaussianSplatRasterize {
731 prep_byte_off: u32,
732 prep_len: u32,
733 meta_byte_off: u32,
734 meta_len: u32,
735 dst_byte_off: u32,
736 dst_len: u32,
737 count: u32,
738 width: u32,
739 height: u32,
740 tile_size: u32,
741 alpha_cutoff: f32,
742 max_splat_steps: u32,
743 transmittance_threshold: f32,
744 max_list_entries: u32,
745 },
746 RmsNormBackwardInput {
747 params: RmsNormBwdParams,
748 },
749 RmsNormBackwardGamma {
750 params: RmsNormBwdParams,
751 },
752 RmsNormBackwardBeta {
753 params: RmsNormBwdParams,
754 },
755 LayerNormBackwardInput {
756 params: LayerNormBwdParams,
757 },
758 LayerNormBackwardGammaPartial {
759 params: LayerNormBwdParams,
760 num_workgroups: u32,
761 },
762 LayerNormBackwardGammaReduce {
763 params: LayerNormBwdParams,
764 },
765 RopeBackward {
766 params: RopeBwdParams,
767 },
768 CumsumBackward {
769 params: CumsumBwdParams,
770 },
771 GatherBackward {
772 params: GatherBwdParams,
773 },
774 FusedResidualLn {
775 params: FusedResidualLnParams,
776 },
777 MatmulQkv {
782 params: MatmulQkvParams,
783 kind: MatmulQkvKind,
784 },
785 FusedResidualLnTee {
789 params: FusedResidualLnTeeParams,
790 },
791 FusedResidualRmsNorm {
792 params: FusedResidualRmsNormParams,
793 },
794}
795
796pub struct WgpuExecutable {
797 graph: Graph,
798 arena: Arena,
799 dequant_scratch_off: usize,
801 schedule: Vec<Step>,
802 input_offsets: HashMap<String, NodeId>,
803 param_offsets: HashMap<String, NodeId>,
804 uniforms: Vec<wgpu::Buffer>,
807 bind_groups: Vec<wgpu::BindGroup>,
808 meta_buffers: Vec<wgpu::Buffer>,
811
812 unresolved: Option<Graph>,
819 last_binding: Option<DimBinding>,
820 pending_params: HashMap<String, Vec<f32>>,
824 pending_param_bytes: HashMap<String, Vec<u8>>,
825 pub(crate) active_extent: Option<(usize, usize)>,
829 uniforms_active_extent: Option<Option<(usize, usize)>>,
839 input_staging_hashes: HashMap<String, u64>,
841 coop_f16_vk: bool,
844 coop_f16_b_param: HashMap<u32, String>,
846 coop_f16_vk_wide_b: HashSet<String>,
848 coop_f16_vk_wide_bind_groups: HashMap<usize, wgpu::BindGroup>,
850 coop_f16_host_activations: Vec<(NodeId, Activation, String)>,
852 stashed_params: HashMap<String, Vec<f32>>,
854 readback_staging: Option<ReadbackStaging>,
856 tiny_readback: Option<TinyReadbackStaging>,
858 dispatch_only: bool,
863 fft_gpu_steps: Vec<crate::fft_dispatch::FftGpuResources>,
865 gpu_handles: HashMap<String, Vec<f32>>,
867 gpu_handle_feeds: HashMap<String, usize>,
868 gpu_handle_resident: HashSet<String>,
870 pending_read_indices: Option<Vec<usize>>,
871 rng: std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
873}
874
875impl Step {
876 pub fn safe_for_active_extent(&self) -> bool {
882 match self {
883 Step::Binary { .. }
884 | Step::Compare { .. }
885 | Step::Unary { .. }
886 | Step::Where { .. }
887 | Step::Fma { .. }
888 | Step::Reduce { .. }
889 | Step::Softmax { .. }
890 | Step::SoftmaxCrossEntropy { .. }
891 | Step::LayerNorm { .. }
892 | Step::FusedResidualLn { .. }
893 | Step::FusedResidualLnTee { .. }
894 | Step::FusedResidualRmsNorm { .. }
895 | Step::Cumsum { .. }
896 | Step::Copy { .. }
897 | Step::ElementwiseRegion { .. }
898 | Step::BatchElementwiseRegion { .. }
899 | Step::Argmax { .. }
900 | Step::TopK { .. }
901 | Step::WelchPeaksGpu { .. }
902 | Step::Sample { .. }
903 | Step::Gather { .. }
904 | Step::GatherAxis { .. }
905 | Step::GatherSplit { .. }
906 | Step::GroupedMatmul { .. }
907 | Step::DequantMatmul { .. }
908 | Step::DequantMatmulGguf { .. }
909 | Step::DequantGroupedMatmulGguf { .. }
910 | Step::GatedDeltaNet { .. }
911 | Step::Lstm { .. }
912 | Step::ConvTranspose2d { .. }
913 | Step::GroupNormHost { .. }
914 | Step::LayerNorm2dHost { .. }
915 | Step::ResizeNearest2xHost { .. }
916 | Step::ReverseHost { .. }
917 | Step::ArgReduceHost { .. }
918 | Step::GruHost { .. }
919 | Step::RnnHost { .. }
920 | Step::Llada2GroupLimitedGate { .. }
921 | Step::UmapKnn { .. }
922 | Step::UmapKnnHost { .. }
923 | Step::MsDeformAttnHost { .. }
924 | Step::Conv1d { .. }
925 | Step::Conv2d { .. }
926 | Step::Conv3d { .. }
927 | Step::Pool1d { .. }
928 | Step::Pool2d { .. }
929 | Step::Pool3d { .. }
930 | Step::ScatterAdd { .. }
931 | Step::BufferCopy { .. } => true,
932 Step::FftGpu { .. } | Step::FftHost { .. } | Step::ScanHost { .. } => true,
937 Step::Im2ColHost { .. }
938 | Step::RngNormalHost { .. }
939 | Step::RngUniformHost { .. }
940 | Step::WelchPeaksHost { .. }
941 | Step::LogMelHost { .. }
942 | Step::LogMelBackwardHost { .. } => true,
943 Step::Matmul { .. } => true,
948 Step::MatmulQkv { .. } => true,
952 Step::CastF32ToF16 { .. } => true,
953 Step::Attention { .. } => true,
959 Step::AttentionBackward { .. } => true,
960 Step::SelectiveScan { .. } => true,
965 Step::Mamba2 { .. } => true,
967 Step::Gru { .. } => true,
970 Step::Rnn { .. } => true,
971 Step::Narrow { .. } => true,
980 Step::Concat { .. } => true,
981 Step::Rope { .. } => true,
987 Step::Transpose { params, .. } => params.bucket_outermost == 1,
993 Step::Expand { params, .. } => params.bucket_outermost == 1,
997 Step::RmsNormBackwardInput { .. }
1000 | Step::RmsNormBackwardGamma { .. }
1001 | Step::RmsNormBackwardBeta { .. }
1002 | Step::LayerNormBackwardInput { .. }
1003 | Step::LayerNormBackwardGammaPartial { .. }
1004 | Step::LayerNormBackwardGammaReduce { .. }
1005 | Step::RopeBackward { .. }
1006 | Step::CumsumBackward { .. }
1007 | Step::GatherBackward { .. } => false,
1008 #[cfg(feature = "splat")]
1009 Step::GaussianSplatRender { .. }
1010 | Step::GaussianSplatRenderBackward { .. }
1011 | Step::GaussianSplatPrepare { .. }
1012 | Step::GaussianSplatRasterize { .. } => false,
1013 }
1014 }
1015}
1016
1017fn fft_dtype_tag(dtype: rlx_ir::DType) -> u32 {
1020 match dtype {
1021 rlx_ir::DType::F32 => 0,
1022 rlx_ir::DType::F64 => 1,
1023 rlx_ir::DType::C64 => 2,
1024 other => panic!("rlx-wgpu Op::Fft: unsupported dtype {other:?}"),
1025 }
1026}
1027
1028fn fft_dtype_from_tag(tag: u32) -> rlx_ir::DType {
1029 match tag {
1030 0 => rlx_ir::DType::F32,
1031 1 => rlx_ir::DType::F64,
1032 2 => rlx_ir::DType::C64,
1033 other => panic!("rlx-wgpu Op::Fft: bad dtype tag {other}"),
1034 }
1035}
1036
1037fn step_name(step: &Step) -> &'static str {
1038 match step {
1039 Step::CastF32ToF16 { .. } => "cast_f32_to_f16",
1040 Step::Matmul { .. } => "matmul",
1041 Step::Binary { .. } => "binary",
1042 Step::Compare { .. } => "compare",
1043 Step::Unary { .. } => "unary",
1044 Step::Where { .. } => "where",
1045 Step::Fma { .. } => "fma",
1046 Step::Reduce { .. } => "reduce",
1047 Step::Softmax { .. } => "softmax",
1048 Step::SoftmaxCrossEntropy { .. } => "softmax_cross_entropy",
1049 Step::LayerNorm { .. } => "layer_norm",
1050 Step::Cumsum { .. } => "cumsum",
1051 Step::FftGpu { .. } => "fft_gpu",
1052 Step::FftHost { .. } => "fft_host",
1053 Step::WelchPeaksHost { .. } => "welch_peaks_host",
1054 Step::LogMelHost { .. } => "log_mel_host",
1055 Step::LogMelBackwardHost { .. } => "log_mel_backward_host",
1056 Step::Im2ColHost { .. } => "im2col_host",
1057 Step::RngNormalHost { .. } => "rng_normal_host",
1058 Step::RngUniformHost { .. } => "rng_uniform_host",
1059 Step::BufferCopy { .. } => "buffer_copy",
1060 Step::Copy { .. } => "copy",
1061 Step::Transpose { .. } => "transpose",
1062 Step::Narrow { .. } => "narrow",
1063 Step::Concat { .. } => "concat",
1064 Step::Gather { .. } => "gather",
1065 Step::GatherAxis { .. } => "gather_axis",
1066 Step::Attention { .. } => "attention",
1067 Step::AttentionBackward { .. } => "attention_bwd",
1068 Step::Rope { .. } => "rope",
1069 Step::Expand { .. } => "expand",
1070 Step::Argmax { .. } => "argmax",
1071 Step::Pool2d { .. } => "pool2d",
1072 Step::Conv2d { .. } => "conv2d",
1073 Step::Pool1d { .. } => "pool1d",
1074 Step::Pool3d { .. } => "pool3d",
1075 Step::Conv1d { .. } => "conv1d",
1076 Step::Conv3d { .. } => "conv3d",
1077 Step::ScatterAdd { .. } => "scatter_add",
1078 Step::TopK { .. } => "topk",
1079 Step::WelchPeaksGpu { .. } => "welch_peaks_gpu",
1080 Step::GroupedMatmul { .. } => "grouped_matmul",
1081 Step::Sample { .. } => "sample",
1082 Step::SelectiveScan { .. } => "selective_scan",
1083 Step::Mamba2 { .. } => "mamba2",
1084 Step::Gru { .. } => "gru",
1085 Step::Rnn { .. } => "rnn",
1086 Step::GruHost { .. } => "gru_host",
1087 Step::RnnHost { .. } => "rnn_host",
1088 Step::DequantMatmul { .. } => "dequant_matmul",
1089 Step::GatherSplit { .. } => "gather_split",
1090 Step::DequantMatmulGguf { .. } => "dequant_matmul_gguf",
1091 Step::DequantGroupedMatmulGguf { .. } => "dequant_grouped_matmul_gguf",
1092 Step::GatedDeltaNet { .. } => "gated_delta_net",
1093 Step::Lstm { .. } => "lstm",
1094 Step::ConvTranspose2d { .. } => "conv_transpose2d",
1095 Step::GroupNormHost { .. } => "group_norm_host",
1096 Step::LayerNorm2dHost { .. } => "layer_norm2d_host",
1097 Step::ResizeNearest2xHost { .. } => "resize_nearest2x_host",
1098 Step::ReverseHost { .. } => "reverse_host",
1099 Step::ArgReduceHost { .. } => "argreduce_host",
1100 Step::Llada2GroupLimitedGate { .. } => "llada2_group_limited_gate",
1101 Step::UmapKnn { .. } => "umap_knn",
1102 Step::UmapKnnHost { .. } => "umap_knn_host",
1103 Step::MsDeformAttnHost { .. } => "ms_deform_attn_host",
1104 Step::ScanHost { .. } => "scan_host",
1105 #[cfg(feature = "splat")]
1106 Step::GaussianSplatRender { .. } => "gaussian_splat_render",
1107 #[cfg(feature = "splat")]
1108 Step::GaussianSplatRenderBackward { .. } => "gaussian_splat_render_backward",
1109 #[cfg(feature = "splat")]
1110 Step::GaussianSplatPrepare { .. } => "gaussian_splat_prepare",
1111 #[cfg(feature = "splat")]
1112 Step::GaussianSplatRasterize { .. } => "gaussian_splat_rasterize",
1113 Step::RmsNormBackwardInput { .. } => "rms_norm_backward_input",
1114 Step::RmsNormBackwardGamma { .. } => "rms_norm_backward_gamma",
1115 Step::RmsNormBackwardBeta { .. } => "rms_norm_backward_beta",
1116 Step::LayerNormBackwardInput { .. } => "layer_norm_backward_input",
1117 Step::LayerNormBackwardGammaPartial { .. } => "layer_norm_backward_gamma_partial",
1118 Step::LayerNormBackwardGammaReduce { .. } => "layer_norm_backward_gamma_reduce",
1119 Step::RopeBackward { .. } => "rope_backward",
1120 Step::CumsumBackward { .. } => "cumsum_backward",
1121 Step::GatherBackward { .. } => "gather_backward",
1122 Step::FusedResidualLn { .. } => "fused_residual_ln",
1123 Step::FusedResidualLnTee { .. } => "fused_residual_ln_tee",
1124 Step::FusedResidualRmsNorm { .. } => "fused_residual_rms_norm",
1125 Step::MatmulQkv { .. } => "matmul_qkv",
1126 Step::ElementwiseRegion { .. } => "elementwise_region",
1127 Step::BatchElementwiseRegion { .. } => "batch_elementwise_region",
1128 }
1129}
1130
1131fn step_is_tail_host(step: &Step) -> bool {
1132 matches!(
1133 step,
1134 Step::WelchPeaksHost { .. } | Step::LogMelHost { .. } | Step::LogMelBackwardHost { .. }
1135 )
1136}
1137
1138fn step_runs_on_host(step: &Step) -> bool {
1139 match step {
1140 Step::GatherSplit { .. }
1141 | Step::DequantMatmulGguf { .. }
1142 | Step::DequantGroupedMatmulGguf { .. }
1143 | Step::GatedDeltaNet { .. }
1144 | Step::Lstm { .. }
1145 | Step::ConvTranspose2d { .. }
1146 | Step::GroupNormHost { .. }
1147 | Step::LayerNorm2dHost { .. }
1148 | Step::ResizeNearest2xHost { .. }
1149 | Step::ReverseHost { .. }
1150 | Step::ArgReduceHost { .. }
1151 | Step::GruHost { .. }
1152 | Step::RnnHost { .. }
1153 | Step::Llada2GroupLimitedGate { .. }
1154 | Step::UmapKnnHost { .. }
1155 | Step::MsDeformAttnHost { .. }
1156 | Step::FftHost { .. }
1157 | Step::ScanHost { .. }
1158 | Step::Im2ColHost { .. }
1159 | Step::RngNormalHost { .. }
1160 | Step::RngUniformHost { .. }
1161 | Step::BufferCopy { .. } => true,
1162 #[cfg(feature = "splat")]
1163 Step::GaussianSplatRender { .. }
1164 | Step::GaussianSplatRenderBackward { .. }
1165 | Step::GaussianSplatPrepare { .. }
1166 | Step::GaussianSplatRasterize { .. } => true,
1167 _ => false,
1168 }
1169}
1170
1171fn binary_op_id(op: BinaryOp) -> u32 {
1172 match op {
1173 BinaryOp::Add => 0,
1174 BinaryOp::Sub => 1,
1175 BinaryOp::Mul => 2,
1176 BinaryOp::Div => 3,
1177 BinaryOp::Max => 4,
1178 BinaryOp::Min => 5,
1179 BinaryOp::Pow => 6,
1180 }
1181}
1182
1183fn compare_op_id(op: CmpOp) -> u32 {
1184 match op {
1185 CmpOp::Eq => 0,
1186 CmpOp::Ne => 1,
1187 CmpOp::Lt => 2,
1188 CmpOp::Le => 3,
1189 CmpOp::Gt => 4,
1190 CmpOp::Ge => 5,
1191 }
1192}
1193
1194fn reduce_op_id(op: ReduceOp) -> u32 {
1195 match op {
1196 ReduceOp::Sum => 0,
1197 ReduceOp::Mean => 1,
1198 ReduceOp::Max => 2,
1199 ReduceOp::Min => 3,
1200 ReduceOp::Prod => 4,
1201 }
1202}
1203
1204fn activation_op_id(act: Activation) -> u32 {
1205 match act {
1206 Activation::Relu => 0,
1207 Activation::Sigmoid => 1,
1208 Activation::Tanh => 2,
1209 Activation::Exp => 3,
1210 Activation::Log => 4,
1211 Activation::Sqrt => 5,
1212 Activation::Rsqrt => 6,
1213 Activation::Neg => 7,
1214 Activation::Abs => 8,
1215 Activation::Gelu => 9,
1216 Activation::Silu => 10,
1217 Activation::GeluApprox => 11,
1218 Activation::Round => 12,
1219 Activation::Sin => 13,
1220 Activation::Cos => 14,
1221 Activation::Tan => 15,
1222 Activation::Atan => 16,
1223 }
1224}
1225
1226mod compile;
1227mod dispatch;
1228mod run;
1229mod set;
1230mod test;
1231
1232impl WgpuExecutable {
1233 pub(crate) fn lazy_compile_for_inputs(&mut self, inputs: &[(&str, &[f32])]) {
1237 let unresolved = self
1238 .unresolved
1239 .as_ref()
1240 .expect("lazy_compile_for_inputs called without an unresolved graph");
1241 let binding = infer_bindings_from_f32_inputs(unresolved, inputs)
1242 .expect("rlx-wgpu lazy compile: could not infer DimBinding from inputs");
1243
1244 if let Some(prev) = &self.last_binding
1246 && same_binding(prev, &binding)
1247 {
1248 return;
1249 }
1250
1251 let resolved = bind_graph(unresolved, &binding);
1253 let original = self.unresolved.take();
1254 let pending_params = std::mem::take(&mut self.pending_params);
1255 let pending_bytes = std::mem::take(&mut self.pending_param_bytes);
1256
1257 let fresh = Self::compile_static_inner(resolved, self.rng.clone());
1258
1259 self.graph = fresh.graph;
1262 self.arena = fresh.arena;
1263 self.dequant_scratch_off = fresh.dequant_scratch_off;
1264 self.schedule = fresh.schedule;
1265 self.input_offsets = fresh.input_offsets;
1266 self.param_offsets = fresh.param_offsets;
1267 self.uniforms = fresh.uniforms;
1268 self.bind_groups = fresh.bind_groups;
1269 self.meta_buffers = fresh.meta_buffers;
1270 self.unresolved = original;
1271 self.last_binding = Some(binding);
1272 self.uniforms_active_extent = None;
1275 self.input_staging_hashes.clear();
1276 self.coop_f16_vk = fresh.coop_f16_vk;
1277 self.coop_f16_b_param = fresh.coop_f16_b_param;
1278 self.coop_f16_vk_wide_bind_groups = fresh.coop_f16_vk_wide_bind_groups;
1279 self.coop_f16_host_activations = fresh.coop_f16_host_activations;
1280
1281 for (name, data) in pending_params {
1283 self.set_param(&name, &data);
1284 }
1285 for (name, data) in pending_bytes {
1286 self.set_param_bytes(&name, &data);
1287 }
1288 }
1289
1290 pub fn rng(&self) -> rlx_ir::RngOptions {
1292 *self.rng.read().expect("rng lock")
1293 }
1294
1295 pub(crate) fn deferred(
1300 graph: Graph,
1301 rng: std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
1302 ) -> Self {
1303 let dev = wgpu_device().expect("rlx-wgpu: no compatible adapter found");
1304 let placeholder = dev.device.create_buffer(&wgpu::BufferDescriptor {
1306 label: Some("rlx-wgpu deferred placeholder"),
1307 size: 16,
1308 usage: wgpu::BufferUsages::STORAGE
1309 | wgpu::BufferUsages::COPY_DST
1310 | wgpu::BufferUsages::COPY_SRC,
1311 mapped_at_creation: false,
1312 });
1313 let arena = Arena {
1314 buffer: placeholder,
1315 f16_buffer: None,
1316 offsets: HashMap::new(),
1317 lens: HashMap::new(),
1318 size: 0,
1319 scratch_off: 0,
1320 scratch_bytes: 0,
1321 };
1322 Self {
1323 graph: graph.clone(),
1324 arena,
1325 dequant_scratch_off: 0,
1326 schedule: Vec::new(),
1327 input_offsets: HashMap::new(),
1328 param_offsets: HashMap::new(),
1329 uniforms: Vec::new(),
1330 bind_groups: Vec::new(),
1331 meta_buffers: Vec::new(),
1332 unresolved: Some(graph),
1333 last_binding: None,
1334 pending_params: HashMap::new(),
1335 pending_param_bytes: HashMap::new(),
1336 active_extent: None,
1337 uniforms_active_extent: None,
1338 input_staging_hashes: HashMap::new(),
1339 coop_f16_vk: false,
1340 coop_f16_b_param: HashMap::new(),
1341 coop_f16_vk_wide_b: HashSet::new(),
1342 coop_f16_vk_wide_bind_groups: HashMap::new(),
1343 coop_f16_host_activations: Vec::new(),
1344 stashed_params: HashMap::new(),
1345 readback_staging: None,
1346 tiny_readback: None,
1347 dispatch_only: false,
1348 fft_gpu_steps: Vec::new(),
1349 gpu_handles: HashMap::new(),
1350 gpu_handle_feeds: HashMap::new(),
1351 gpu_handle_resident: HashSet::new(),
1352 pending_read_indices: None,
1353 rng,
1354 }
1355 }
1356
1357 pub(crate) fn all_safe_for_active(&self) -> bool {
1358 self.schedule.iter().all(|s| s.safe_for_active_extent())
1359 }
1360
1361 pub fn debug_first_nan_node(
1366 &mut self,
1367 inputs: &[(&str, &[f32])],
1368 ) -> Option<(usize, String, String)> {
1369 let _ = self.run(inputs);
1370 let dev = wgpu_device().expect("rlx-wgpu: device gone");
1371 let mut prev_summary = String::from("(none)");
1372 for (i, node) in self.graph.nodes().iter().enumerate() {
1373 if !self.arena.has(node.id) {
1374 continue;
1375 }
1376 let elems = node.shape.num_elements().unwrap_or(0);
1377 if elems == 0 {
1378 continue;
1379 }
1380 let data = self.arena.read_f32(&dev.device, &dev.queue, node.id);
1381 let nan_count = data.iter().filter(|v| v.is_nan()).count();
1382 let inf_count = data.iter().filter(|v| v.is_infinite()).count();
1383 if nan_count > 0 || inf_count > 0 {
1384 return Some((i, format!("{:?}", node.op), prev_summary));
1385 }
1386 let max = data.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1387 let min = data.iter().copied().fold(f32::INFINITY, f32::min);
1388 let abs_max = data.iter().map(|v| v.abs()).fold(0.0_f32, f32::max);
1389 prev_summary = format!(
1390 "node #{i} {:?} shape={:?} min={min:.6e} max={max:.6e} |max|={abs_max:.6e}",
1391 node.op,
1392 node.shape
1393 .dims()
1394 .iter()
1395 .map(|d| format!("{d:?}"))
1396 .collect::<Vec<_>>()
1397 );
1398 }
1399 None
1400 }
1401
1402 pub fn output_dtypes(&self) -> Vec<rlx_ir::DType> {
1406 self.graph
1407 .outputs
1408 .iter()
1409 .map(|&id| self.graph.node(id).shape.dtype())
1410 .collect()
1411 }
1412
1413 pub(crate) fn dump_node_stats_if_requested(&self, dev: &crate::device::WgpuDevice) {
1414 if !rlx_ir::env::flag("RLX_WGPU_DUMP_NODES") {
1415 return;
1416 }
1417 let flat_probe = rlx_ir::env::parse_or::<usize>("RLX_WGPU_DUMP_FLAT", usize::MAX);
1418 let limit = rlx_ir::env::parse_or("RLX_WGPU_DUMP_NODES_LIMIT", 40usize);
1419 eprintln!(
1420 "[rlx-wgpu-dump] per-node max |x| (topo order, limit={limit}{})",
1421 if flat_probe != usize::MAX {
1422 format!(", flat[{flat_probe}]")
1423 } else {
1424 String::new()
1425 }
1426 );
1427 let mut shown = 0usize;
1428 for (i, node) in self.graph.nodes().iter().enumerate() {
1429 if !self.arena.has(node.id) {
1430 continue;
1431 }
1432 if matches!(
1433 node.op,
1434 rlx_ir::Op::Input { .. }
1435 | rlx_ir::Op::Param { .. }
1436 | rlx_ir::Op::Constant { .. }
1437 | rlx_ir::Op::Reshape { .. }
1438 | rlx_ir::Op::Cast { .. }
1439 ) {
1440 continue;
1441 }
1442 let data = self.arena.read_f32(&dev.device, &dev.queue, node.id);
1443 let max = data.iter().fold(0.0f32, |m, &v| m.max(v.abs()));
1444 let nz = data.iter().filter(|&&v| v != 0.0).count();
1445 let flat_s = if flat_probe < data.len() {
1446 format!(" flat[{flat_probe}]={:.6}", data[flat_probe])
1447 } else {
1448 String::new()
1449 };
1450 eprintln!(
1451 " [{i:>3}] {:?} max={max:.6} nonzero={}/{}{flat_s}",
1452 node.op,
1453 nz,
1454 data.len()
1455 );
1456 shown += 1;
1457 if shown >= limit {
1458 break;
1459 }
1460 }
1461 }
1462
1463 pub fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
1464 if !self.input_offsets.contains_key(name) {
1465 return false;
1466 }
1467 self.gpu_handle_resident.remove(name);
1468 self.gpu_handles.insert(name.to_string(), data.to_vec());
1469 true
1470 }
1471
1472 pub fn has_gpu_handle(&self, name: &str) -> bool {
1473 self.gpu_handles.contains_key(name)
1474 }
1475
1476 pub fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
1477 if let Some(&out_idx) = self.gpu_handle_feeds.get(name) {
1478 if out_idx < self.graph.outputs.len() {
1479 let id = self.graph.outputs[out_idx];
1480 if self.arena.has(id) {
1481 let dev = wgpu_device().expect("rlx-wgpu: device gone");
1482 return Some(self.arena.read_f32(&dev.device, &dev.queue, id));
1483 }
1484 }
1485 }
1486 if self.gpu_handle_resident.contains(name) {
1487 if let Some(&id) = self.input_offsets.get(name) {
1488 if self.arena.has(id) {
1489 let dev = wgpu_device().expect("rlx-wgpu: device gone");
1490 return Some(self.arena.read_f32(&dev.device, &dev.queue, id));
1491 }
1492 }
1493 }
1494 self.gpu_handles.get(name).cloned()
1495 }
1496
1497 pub fn clone_for_cache(&self) -> Self {
1499 let graph = self
1500 .unresolved
1501 .clone()
1502 .unwrap_or_else(|| self.graph.clone());
1503 let mut exe = Self::compile_rng(graph, self.rng());
1504 for (k, v) in &self.stashed_params {
1505 exe.set_param(k, v);
1506 }
1507 for (k, v) in &self.pending_params {
1508 exe.set_param(k, v);
1509 }
1510 for (k, v) in &self.pending_param_bytes {
1511 exe.set_param_bytes(k, v);
1512 }
1513 for (k, v) in &self.gpu_handles {
1514 exe.bind_gpu_handle(k, v);
1515 }
1516 for (k, &idx) in &self.gpu_handle_feeds {
1517 exe.set_gpu_handle_feed(k, idx);
1518 }
1519 exe.set_active_extent(self.active_extent);
1520 exe.set_rng(self.rng());
1521 exe
1522 }
1523
1524 pub(crate) fn readback_plan(&self) -> Vec<usize> {
1525 let n = self.graph.outputs.len();
1526 if self.pending_read_indices.is_none() && self.gpu_handle_feeds.is_empty() {
1527 return (0..n).collect();
1528 }
1529 if let Some(ref want) = self.pending_read_indices {
1530 let mut v: Vec<_> = want.to_vec();
1531 v.sort_unstable();
1532 return v;
1533 }
1534 (0..n).collect()
1535 }
1536
1537 pub(crate) fn propagate_gpu_handle_feeds_on_gpu(
1538 &mut self,
1539 dev: &crate::device::WgpuDevice,
1540 enc: &mut wgpu::CommandEncoder,
1541 ) {
1542 let extent = self.active_extent;
1543 let feeds: Vec<(String, usize)> = self
1544 .gpu_handle_feeds
1545 .iter()
1546 .map(|(n, &i)| (n.clone(), i))
1547 .collect();
1548 for (name, out_idx) in feeds {
1549 if out_idx >= self.graph.outputs.len() {
1550 continue;
1551 }
1552 let out_id = self.graph.outputs[out_idx];
1553 let Some(&in_id) = self.input_offsets.get(name.as_str()) else {
1554 continue;
1555 };
1556 if in_id != out_id {
1557 let out_bytes = self.arena.len_of(out_id);
1558 let copy_bytes = match extent {
1559 Some((actual, upper)) if upper > 0 => {
1560 let stride = (out_bytes / (upper + 1)).max(4);
1561 (actual * stride).min(out_bytes)
1562 }
1563 _ => out_bytes,
1564 };
1565 self.dispatch_arena_copy_bytes(dev, enc, out_id, in_id, copy_bytes);
1566 }
1567 self.gpu_handle_resident.insert(name.clone());
1568 self.gpu_handles.insert(name.clone(), Vec::new());
1569 }
1570 }
1571
1572 pub(crate) fn stage_gpu_handle_inputs(
1573 &mut self,
1574 dev: &crate::device::WgpuDevice,
1575 inputs: &[(&str, &[f32])],
1576 ) {
1577 for (name, data) in &self.gpu_handles {
1578 if self.gpu_handle_resident.contains(name) || inputs.iter().any(|(n, _)| n == name) {
1579 continue;
1580 }
1581 if let Some(&id) = self.input_offsets.get(name.as_str())
1582 && self.arena.has(id)
1583 {
1584 self.arena.write_f32(&dev.queue, id, data);
1585 self.input_staging_hashes.remove(name);
1586 }
1587 }
1588 }
1589
1590 pub(crate) fn pack_readback_outputs(
1591 &mut self,
1592 plan: &[usize],
1593 partial: Vec<Vec<f32>>,
1594 ) -> Vec<Vec<f32>> {
1595 if self.pending_read_indices.is_none() {
1596 for (pos, &out_i) in plan.iter().enumerate() {
1597 if let Some(data) = partial.get(pos) {
1598 for (name, &feed_i) in &self.gpu_handle_feeds {
1599 if feed_i == out_i {
1600 self.gpu_handles.insert(name.clone(), data.clone());
1601 }
1602 }
1603 }
1604 }
1605 }
1606 if self.pending_read_indices.is_none() && plan.len() == self.graph.outputs.len() {
1607 return partial;
1608 }
1609 let want = self.pending_read_indices.as_deref().unwrap_or(plan);
1610 let mut by_idx = std::collections::HashMap::new();
1611 for (pos, &i) in plan.iter().enumerate() {
1612 if let Some(d) = partial.get(pos) {
1613 by_idx.insert(i, d.clone());
1614 }
1615 }
1616 want.iter()
1617 .map(|&i| {
1618 by_idx
1619 .get(&i)
1620 .cloned()
1621 .expect("readback plan missing output")
1622 })
1623 .collect()
1624 }
1625}
1626
1627fn dispatch_prologue_nchw(w: u32, h: u32, nc: u32) -> (u32, u32, u32) {
1634 (w.div_ceil(8).max(1), h.div_ceil(8).max(1), nc.max(1))
1635}
1636
1637fn dispatch_dims(threads_total: u32, workgroup_size: u32) -> (u32, u32, u32) {
1638 let groups = threads_total.div_ceil(workgroup_size);
1639 if groups <= 65535 {
1640 (groups, 1, 1)
1641 } else {
1642 let gx = 65535u32;
1643 let gy = groups.div_ceil(gx);
1644 (gx, gy, 1)
1645 }
1646}
1647
1648fn coop_f16_vk_eligible(dev: &wgpu::Device, m: u32, k: u32, n: u32) -> bool {
1670 if rlx_ir::env::flag("RLX_WGPU_NO_COOP_F16_VK")
1671 || rlx_ir::env::flag("RLX_WGPU_COOP_F16_VK_DISABLE")
1672 {
1673 return false;
1674 }
1675 if !rlx_ir::env::flag("RLX_WGPU_COOP_F16_VK_ENABLE") {
1676 return false;
1677 }
1678 m.is_multiple_of(16)
1679 && k.is_multiple_of(16)
1680 && n.is_multiple_of(16)
1681 && dev
1682 .features()
1683 .contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX)
1684 && dev.features().contains(wgpu::Features::SHADER_F16)
1685 && crate::device::coop_discrete_backend()
1686 && crate::device::coop_f16_16x16_supported()
1687}
1688
1689fn step_needs_pass_flush(step: &Step, prev: &Step) -> bool {
1690 match step {
1691 Step::CastF32ToF16 { .. } => matches!(
1692 prev,
1693 Step::Unary {
1694 f16_mirror: false,
1695 ..
1696 }
1697 ),
1698 Step::Matmul {
1699 compute_precision: MatmulCompute::CoopF16Vk,
1700 ..
1701 }
1702 | Step::MatmulQkv {
1703 kind: MatmulQkvKind::CoopF16Vk,
1704 ..
1705 } => matches!(prev, Step::Unary { .. } | Step::CastF32ToF16 { .. }),
1706 _ => false,
1707 }
1708}
1709
1710fn dispatch_wide_f32_matmul(
1711 pass: &mut wgpu::ComputePass<'_>,
1712 mm_w_active: &Kernel,
1713 mm_k: &Kernel,
1714 m_s: u32,
1715 n: u32,
1716 batch: u32,
1717) {
1718 let backend = wgpu_device()
1733 .map(|d| d.backend)
1734 .unwrap_or(wgpu::Backend::Noop);
1735 let is_vulkan_dx12 = matches!(backend, wgpu::Backend::Vulkan | wgpu::Backend::Dx12);
1736 let prefer_small_for_m = is_vulkan_dx12 && m_s < 64;
1737 let use_wide = !prefer_small_for_m && m_s >= 32 && n >= 64;
1738 if use_wide {
1739 pass.set_pipeline(&mm_w_active.pipeline);
1740 let (gx, gy) = if is_vulkan_dx12 {
1741 (n.div_ceil(64), m_s.div_ceil(64))
1742 } else {
1743 (n.div_ceil(64), m_s.div_ceil(32))
1744 };
1745 pass.dispatch_workgroups(gx, gy, batch);
1746 } else {
1747 pass.set_pipeline(&mm_k.pipeline);
1748 pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), batch);
1749 }
1750}
1751
1752fn coop_f16_vk_bind_group(exe: &WgpuExecutable, gpu_bi: usize, use_wide: bool) -> &wgpu::BindGroup {
1753 if use_wide {
1754 exe.coop_f16_vk_wide_bind_groups
1755 .get(&gpu_bi)
1756 .unwrap_or(&exe.bind_groups[gpu_bi])
1757 } else {
1758 &exe.bind_groups[gpu_bi]
1759 }
1760}
1761
1762fn require_equal_shapes(graph: &Graph, ids: &[NodeId], op_name: &str) {
1763 let s0 = graph.node(ids[0]).shape.num_elements().unwrap_or(0);
1764 for &id in &ids[1..] {
1765 let si = graph.node(id).shape.num_elements().unwrap_or(0);
1766 if si != s0 {
1767 panic!(
1768 "rlx-wgpu {op_name}: broadcasting not yet implemented; \
1769 inputs must have the same element count (got {s0} vs {si})"
1770 );
1771 }
1772 }
1773}
1774
1775fn arena_whole_arena_bind(arena: &Arena, max_binding: u64) -> Option<(u64, u64)> {
1777 let need = arena.size as u64;
1778 if need > max_binding {
1779 return None;
1780 }
1781 let buf_bytes = arena.buffer.size();
1783 let size = need.min(buf_bytes).max(256);
1784 Some((0, size))
1785}
1786
1787fn arena_window_for_nodes(dev: &wgpu::Device, arena: &Arena, ids: &[NodeId]) -> (u64, u64) {
1788 const ALIGN: u64 = 256;
1790 let max_binding = dev.limits().max_storage_buffer_binding_size;
1791 if let Some(w) = arena_whole_arena_bind(arena, max_binding) {
1792 return w;
1793 }
1794 let mut lo: u64 = u64::MAX;
1795 let mut hi: u64 = 0;
1796 for &id in ids {
1797 let off = arena.offset(id) as u64;
1798 let len = arena.len_of(id) as u64;
1799 lo = lo.min(off);
1800 hi = hi.max(off.saturating_add(len));
1801 }
1802 if lo == u64::MAX {
1803 return (0, max_binding.max(256));
1804 }
1805 let span = hi.saturating_sub(lo).max(1);
1806 if span > max_binding {
1807 let mut details = String::new();
1808 for &id in ids.iter().take(6) {
1809 let off = arena.offset(id);
1810 let len = arena.len_of(id);
1811 details.push_str(&format!(" id={id:?}@{off}+{len};"));
1812 }
1813 panic!(
1814 "rlx-wgpu: op needs {} bytes of arena span (>{});{}",
1815 span, max_binding, details
1816 );
1817 }
1818 let mut base = (lo / ALIGN) * ALIGN;
1819 let mut size = span.div_ceil(ALIGN) * ALIGN;
1822 size = size.max(256).min(max_binding);
1823 if base.saturating_add(size) > arena.size as u64 {
1824 base = (arena.size as u64).saturating_sub(size);
1825 base = (base / ALIGN) * ALIGN;
1826 }
1827 if base > lo || base.saturating_add(size) < hi {
1828 base = (lo / ALIGN) * ALIGN;
1829 size = hi.saturating_sub(base).div_ceil(ALIGN) * ALIGN;
1830 size = size.max(256).min(max_binding);
1831 if base.saturating_add(size) > arena.size as u64 {
1832 base = hi.saturating_sub(size);
1833 base = (base / ALIGN) * ALIGN;
1834 }
1835 }
1836 (base, size)
1837}
1838
1839fn arena_local_off_f32(arena: &Arena, id: NodeId, base: u64) -> u32 {
1840 (((arena.offset(id) as u64).saturating_sub(base)) / 4) as u32
1841}
1842
1843#[allow(clippy::too_many_arguments)]
1852fn run_gather_split(
1853 arena: &Arena,
1854 device: &wgpu::Device,
1855 queue: &wgpu::Queue,
1856 n_out: u32,
1857 n_idx: u32,
1858 dim: u32,
1859 vocab: u32,
1860 table_byte_off: usize,
1861 idx_byte_off: usize,
1862 out_byte_off: usize,
1863) {
1864 const ALIGN: u64 = 256;
1865 let arena_size = arena.size as u64;
1866 let max_bind = device.limits().max_storage_buffer_binding_size;
1867
1868 let t0 = table_byte_off as u64;
1870 let t_bytes = (vocab as u64) * (dim as u64) * 4;
1871 let t_base = (t0 / ALIGN) * ALIGN;
1872 let t_size = ((t0 + t_bytes - t_base).div_ceil(16) * 16).min(arena_size - t_base);
1873
1874 let i0 = idx_byte_off as u64;
1876 let i_bytes = ((n_idx as u64) * 4).max(4);
1877 let i_base = (i0 / ALIGN) * ALIGN;
1878 let i_size = ((i0 + i_bytes - i_base).div_ceil(16) * 16).min(arena_size - i_base);
1879
1880 assert!(
1881 t_size <= max_bind && i_size <= max_bind,
1882 "rlx-wgpu gather_split: window too large (table={t_size}, idx={i_size}, max={max_bind})"
1883 );
1884
1885 let out_bytes = ((n_out as u64) * 4).max(4);
1887 let out_buf = device.create_buffer(&wgpu::BufferDescriptor {
1888 label: Some("rlx-wgpu gather_split out"),
1889 size: out_bytes.div_ceil(16) * 16,
1890 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
1891 mapped_at_creation: false,
1892 });
1893
1894 let p = GatherParams {
1895 n_out,
1896 n_idx,
1897 dim,
1898 vocab,
1899 in_off: ((t0 - t_base) / 4) as u32,
1900 idx_off: ((i0 - i_base) / 4) as u32,
1901 out_off: 0,
1902 _p0: 0,
1903 };
1904 let u = device.create_buffer(&wgpu::BufferDescriptor {
1905 label: Some("rlx-wgpu gather_split uniform"),
1906 size: std::mem::size_of::<GatherParams>() as u64,
1907 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1908 mapped_at_creation: false,
1909 });
1910 queue.write_buffer(&u, 0, bytemuck::bytes_of(&p));
1911
1912 let gk = gather_split_kernel(device);
1913 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1914 label: Some("rlx-wgpu gather_split bg"),
1915 layout: &gk.bgl,
1916 entries: &[
1917 wgpu::BindGroupEntry {
1918 binding: 0,
1919 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1920 buffer: &arena.buffer,
1921 offset: t_base,
1922 size: wgpu::BufferSize::new(t_size),
1923 }),
1924 },
1925 wgpu::BindGroupEntry {
1926 binding: 1,
1927 resource: u.as_entire_binding(),
1928 },
1929 wgpu::BindGroupEntry {
1930 binding: 2,
1931 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1932 buffer: &arena.buffer,
1933 offset: i_base,
1934 size: wgpu::BufferSize::new(i_size),
1935 }),
1936 },
1937 wgpu::BindGroupEntry {
1938 binding: 3,
1939 resource: out_buf.as_entire_binding(),
1940 },
1941 ],
1942 });
1943
1944 let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1945 label: Some("rlx-wgpu gather_split"),
1946 });
1947 {
1948 let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
1949 label: Some("rlx-wgpu gather_split pass"),
1950 ..Default::default()
1951 });
1952 pass.set_pipeline(&gk.pipeline);
1953 pass.set_bind_group(0, &bg, &[]);
1954 let (gx, gy, gz) = dispatch_dims(n_out, 64);
1955 pass.dispatch_workgroups(gx, gy, gz);
1956 }
1957 enc.copy_buffer_to_buffer(&out_buf, 0, &arena.buffer, out_byte_off as u64, out_bytes);
1959 queue.submit(std::iter::once(enc.finish()));
1960}
1961
1962fn arena_tensor_in_window(arena: &Arena, id: NodeId, base: u64, size: u64) -> bool {
1963 let src = arena.offset(id) as u64;
1964 let len = arena.len_of(id) as u64;
1965 src >= base && src.saturating_add(len) <= base.saturating_add(size)
1966}
1967
1968fn arena_tensors_overlap(arena: &Arena, a: NodeId, b: NodeId) -> bool {
1970 if a == b {
1971 return true;
1972 }
1973 let (a0, al) = (arena.offset(a) as u64, arena.len_of(a) as u64);
1974 let (b0, bl) = (arena.offset(b) as u64, arena.len_of(b) as u64);
1975 if al == 0 || bl == 0 {
1976 return false;
1977 }
1978 let a1 = a0.saturating_add(al);
1979 let b1 = b0.saturating_add(bl);
1980 a0 < b1 && b0 < a1
1981}
1982
1983fn matmul_b_from_f16(precision: MatmulCompute, b_is_param: bool) -> bool {
1987 b_is_param
1988 && matches!(
1989 precision,
1990 MatmulCompute::F16 | MatmulCompute::Coop16 | MatmulCompute::CoopF16Vk
1991 )
1992}
1993
1994fn arena_matmul_bind_window(
1997 device: &wgpu::Device,
1998 arena: &Arena,
1999 graph: &Graph,
2000 param_offsets: &HashMap<String, NodeId>,
2001 out_id: NodeId,
2002 a_id: NodeId,
2003 b_id: NodeId,
2004 b_in_arena: bool,
2005) -> (u64, u64, bool) {
2006 let max_binding = device.limits().max_storage_buffer_binding_size;
2007 if let Some((base, size)) = arena_whole_arena_bind(arena, max_binding) {
2008 return (base, size, false);
2009 }
2010 if !b_in_arena {
2011 let (base, size) = arena_window_for_nodes(device, arena, &[out_id, a_id]);
2018 return (base, size, false);
2019 }
2020 let ids = [out_id, a_id, b_id];
2021 let all_fits = arena_span_bytes(arena, &ids) <= max_binding;
2022 let b_bytes = arena.len_of(b_id) as u64;
2023 let b_is_param = tensor_is_graph_param(graph, param_offsets, b_id);
2024 let param_anchor =
2025 b_is_param && b_bytes <= max_binding && (!all_fits || b_bytes > ARENA_STAGE_CAP);
2026 let (mut base, mut size) = if param_anchor {
2027 arena_window_for_nodes(device, arena, &[b_id])
2028 } else if all_fits {
2029 arena_window_for_nodes(device, arena, &ids)
2030 } else {
2031 arena_window_for_nodes(device, arena, &[out_id])
2032 };
2033 let param_anchor = param_anchor
2034 || (b_is_param
2035 && b_bytes <= max_binding
2036 && !arena_tensor_in_window(arena, b_id, base, size));
2037 if param_anchor && !arena_tensor_in_window(arena, b_id, base, size) {
2038 (base, size) = arena_window_for_nodes(device, arena, &[b_id]);
2039 }
2040 (base, size, param_anchor)
2041}
2042
2043fn arena_expand_bind_window(
2046 arena: &Arena,
2047 ids: &[NodeId],
2048 base: &mut u64,
2049 size: &mut u64,
2050 max_binding: u64,
2051) {
2052 const ALIGN: u64 = 256;
2053 let mut lo = *base;
2054 let mut hi = base.saturating_add(*size);
2055 for &id in ids {
2056 let off = arena.offset(id) as u64;
2057 let len = arena.len_of(id) as u64;
2058 lo = lo.min(off);
2059 hi = hi.max(off.saturating_add(len));
2060 }
2061 let span = hi.saturating_sub(lo).max(1);
2062 if span > max_binding {
2063 return;
2064 }
2065 *base = (lo / ALIGN) * ALIGN;
2066 *size = span.div_ceil(ALIGN) * ALIGN;
2067 *size = (*size).max(256).min(max_binding);
2068 if (*base).saturating_add(*size) > arena.size as u64 {
2069 *base = (arena.size as u64).saturating_sub(*size);
2070 *base = (*base / ALIGN) * ALIGN;
2071 }
2072}
2073
2074fn arena_off_in_bind_window(
2075 graph: &Graph,
2076 param_offsets: &HashMap<String, NodeId>,
2077 device: &wgpu::Device,
2078 arena: &Arena,
2079 schedule: &mut Vec<Step>,
2080 scratch: &mut u64,
2081 id: NodeId,
2082 base: &mut u64,
2083 size: &mut u64,
2084) -> u32 {
2085 let max_binding = device.limits().max_storage_buffer_binding_size;
2086 if let Some((b, s)) = arena_whole_arena_bind(arena, max_binding) {
2087 *base = b;
2088 *size = s;
2089 return arena_local_off_f32(arena, id, b);
2090 }
2091 if arena_tensor_in_window(arena, id, *base, *size) {
2092 arena_local_off_f32(arena, id, *base)
2093 } else {
2094 let len = arena.len_of(id) as u64;
2095 if tensor_is_graph_param(graph, param_offsets, id) && len > max_binding {
2096 panic!(
2097 "rlx-wgpu: param node {:?} ({} bytes) exceeds max_storage_buffer_binding_size \
2098 ({max_binding}); split weights or use f16 shadow binds",
2099 id, len
2100 );
2101 }
2102 if len > ARENA_STAGE_CAP {
2103 let op = &graph.node(id).op;
2104 panic!(
2105 "rlx-wgpu: bind_window would stage {} bytes for {:?} op={op:?} \
2106 (off={}, base={}, bind_size={})",
2107 len,
2108 id,
2109 arena.offset(id),
2110 *base,
2111 *size,
2112 );
2113 }
2114 arena_off_in_window_or_stage(arena, schedule, scratch, base, size, max_binding, id)
2115 }
2116}
2117
2118fn arena_multi_op_window(
2122 dev: &wgpu::Device,
2123 arena: &Arena,
2124 graph: &Graph,
2125 param_offsets: &HashMap<String, NodeId>,
2126 _schedule: &mut Vec<Step>,
2127 scratch: &mut u64,
2128 ids: &[NodeId],
2129) -> (u64, u64, bool) {
2130 let max_binding = dev.limits().max_storage_buffer_binding_size;
2131 if let Some((base, size)) = arena_whole_arena_bind(arena, max_binding) {
2132 *scratch = arena.scratch_off as u64;
2133 return (base, size, false);
2134 }
2135 let param_anchor = if arena_span_bytes(arena, ids) > max_binding {
2136 ids.iter()
2137 .find(|&&id| {
2138 let nbytes = arena.len_of(id) as u64;
2139 tensor_is_graph_param(graph, param_offsets, id) && nbytes <= max_binding
2140 })
2141 .copied()
2142 } else {
2143 None
2144 };
2145 let mut param_anchored = param_anchor.is_some();
2146 let (mut base, mut size) = if arena_span_bytes(arena, ids) <= max_binding {
2147 arena_window_for_nodes(dev, arena, ids)
2148 } else if let Some(id) = param_anchor {
2149 arena_window_for_nodes(dev, arena, &[id])
2150 } else {
2151 arena_window_for_nodes(dev, arena, &[ids[0]])
2152 };
2153 if let Some(id) = param_anchor {
2154 if !arena_tensor_in_window(arena, id, base, size) {
2155 (base, size) = arena_window_for_nodes(dev, arena, &[id]);
2156 }
2157 param_anchored = true;
2158 } else {
2159 for &id in ids {
2160 let nbytes = arena.len_of(id) as u64;
2161 if tensor_is_graph_param(graph, param_offsets, id)
2162 && nbytes <= max_binding
2163 && !arena_tensor_in_window(arena, id, base, size)
2164 {
2165 (base, size) = arena_window_for_nodes(dev, arena, &[id]);
2166 param_anchored = true;
2167 break;
2168 }
2169 }
2170 }
2171 *scratch = arena.scratch_off as u64;
2172 if param_anchored {
2173 arena_ensure_scratch_in_window(scratch, base, size);
2174 }
2175 (base, size, param_anchored)
2176}
2177
2178fn arena_bind_window_covering_scratch_if_needed(
2179 arena: &Arena,
2180 base: u64,
2181 size: u64,
2182 scratch: u64,
2183) -> u64 {
2184 if scratch <= arena.scratch_off as u64 {
2187 return base;
2188 }
2189 if scratch >= base && scratch.saturating_add(ARENA_STAGE_CAP) <= base.saturating_add(size) {
2190 return base;
2191 }
2192 arena_window_covering_scratch(arena, base, size)
2193}
2194
2195fn arena_ensure_scratch_in_window(scratch: &mut u64, base: u64, size: u64) {
2198 let cap = ARENA_STAGE_CAP.min(size);
2199 let end = base.saturating_add(size);
2200 if *scratch < base || scratch.saturating_add(cap) > end {
2201 *scratch = end.saturating_sub(cap);
2202 *scratch = (*scratch / 256) * 256;
2203 }
2204}
2205
2206#[allow(dead_code)]
2207fn arena_off_for_window(
2208 arena: &Arena,
2209 schedule: &mut Vec<Step>,
2210 scratch: &mut u64,
2211 id: NodeId,
2212 _window_ids: &[NodeId],
2213 mut base: u64,
2214 mut size: u64,
2215 max_binding: u64,
2216 _fits_in_one_binding: bool,
2217) -> u32 {
2218 let src = arena.offset(id) as u64;
2219 let len = arena.len_of(id) as u64;
2220 if src >= base && src.saturating_add(len) <= base.saturating_add(size) {
2221 arena_local_off_f32(arena, id, base)
2222 } else {
2223 arena_off_in_window_or_stage(
2224 arena,
2225 schedule,
2226 scratch,
2227 &mut base,
2228 &mut size,
2229 max_binding,
2230 id,
2231 )
2232 }
2233}
2234
2235fn f16_shadow_bind_range(arena_base: u64, arena_size: u64, f16_buf_bytes: u64) -> (u64, u64) {
2237 const ALIGN: u64 = 256;
2238 let mut base = (arena_base / 2 / ALIGN) * ALIGN;
2239 let mut size = (arena_size / 2).div_ceil(ALIGN) * ALIGN;
2240 size = size.max(256).min(f16_buf_bytes);
2241 if base.saturating_add(size) > f16_buf_bytes {
2242 base = f16_buf_bytes.saturating_sub(size);
2243 base = (base / ALIGN) * ALIGN;
2244 }
2245 (base, size)
2246}
2247
2248fn f16_weight_bind_range(
2251 dev: &wgpu::Device,
2252 f16_buf_bytes: u64,
2253 b_off: u32,
2254 k: u32,
2255 n: u32,
2256 batch: u32,
2257 b_batch_stride: u32,
2258) -> (u64, u64, u32) {
2259 const ALIGN: u64 = 256;
2260 let max_binding = dev.limits().max_storage_buffer_binding_size;
2261 let b0 = b_off as u64;
2262 let span = (k as u64).saturating_mul(n as u64);
2263 let batch_n = batch.max(1) as u64;
2264 let stride = if batch_n > 1 {
2265 b_batch_stride as u64
2266 } else {
2267 span
2268 };
2269 let hi_elems = b0
2270 .saturating_add((batch_n - 1).saturating_mul(stride))
2271 .saturating_add(span);
2272 let lo_byte = b0.saturating_mul(2);
2273 let hi_byte = hi_elems.saturating_mul(2).saturating_add(8);
2274 let need = hi_byte.saturating_sub(lo_byte).max(1);
2275 if need > max_binding {
2276 panic!(
2277 "rlx-wgpu: f16 weight region needs {need} bytes (> {max_binding}); \
2278 matmul k={k} n={n} batch={batch}"
2279 );
2280 }
2281 let mut base = (lo_byte / ALIGN) * ALIGN;
2282 let mut size = need.div_ceil(ALIGN) * ALIGN;
2283 size = size.max(256).min(max_binding).min(f16_buf_bytes);
2284 if base.saturating_add(size) < hi_byte {
2285 base = hi_byte.saturating_sub(size);
2286 base = (base / ALIGN) * ALIGN;
2287 }
2288 if base.saturating_add(size) > f16_buf_bytes {
2289 base = f16_buf_bytes.saturating_sub(size);
2290 base = (base / ALIGN) * ALIGN;
2291 }
2292 let rebased = b_off.saturating_sub((base / 2) as u32);
2293 (base, size, rebased)
2294}
2295
2296const ARENA_STAGE_CAP: u64 = 256 * 1024 * 1024;
2297
2298const CONV2D_TILE: u32 = 4;
2301
2302fn arena_off_in_window_or_stage(
2305 arena: &Arena,
2306 schedule: &mut Vec<Step>,
2307 scratch: &mut u64,
2308 base: &mut u64,
2309 size: &mut u64,
2310 max_binding: u64,
2311 id: NodeId,
2312) -> u32 {
2313 let src = arena.offset(id) as u64;
2314 let len = arena.len_of(id) as u64;
2315 if src >= *base && src.saturating_add(len) <= (*base).saturating_add(*size) {
2316 return arena_local_off_f32(arena, id, *base);
2317 }
2318 if len > ARENA_STAGE_CAP {
2319 panic!(
2320 "rlx-wgpu: cannot stage {} bytes for node {:?} (cap {ARENA_STAGE_CAP})",
2321 len, id
2322 );
2323 }
2324 let aligned = len.div_ceil(256) * 256;
2325 let dst = *scratch;
2326 *scratch = scratch.saturating_add(aligned);
2327 schedule.push(Step::BufferCopy {
2328 src_byte_off: src,
2329 dst_byte_off: dst,
2330 bytes: len as u32,
2331 });
2332 let lo = (*base).min(dst);
2333 let hi = (*base)
2334 .saturating_add(*size)
2335 .max(dst.saturating_add(aligned));
2336 let span = hi.saturating_sub(lo).max(1);
2337 if span <= max_binding {
2338 const ALIGN: u64 = 256;
2339 *base = (lo / ALIGN) * ALIGN;
2340 *size = span.div_ceil(ALIGN) * ALIGN;
2341 *size = (*size).max(256).min(max_binding);
2342 if (*base).saturating_add(*size) > arena.size as u64 {
2343 *base = (arena.size as u64).saturating_sub(*size);
2344 *base = (*base / ALIGN) * ALIGN;
2345 }
2346 }
2347 if arena_tensor_in_window(arena, id, *base, *size) {
2348 arena_local_off_f32(arena, id, *base)
2349 } else {
2350 ((dst.saturating_sub(*base)) / 4) as u32
2351 }
2352}
2353
2354fn arena_window_covering_scratch(arena: &Arena, base: u64, size: u64) -> u64 {
2356 let scratch = arena.scratch_off as u64;
2357 if scratch >= base && scratch.saturating_add(ARENA_STAGE_CAP) <= base.saturating_add(size) {
2358 return base;
2359 }
2360 let new_base = (arena.size as u64).saturating_sub(size);
2361 (new_base / 256) * 256
2362}
2363
2364fn arena_span_bytes(arena: &Arena, ids: &[NodeId]) -> u64 {
2365 let mut lo: u64 = u64::MAX;
2366 let mut hi: u64 = 0;
2367 for &id in ids {
2368 let off = arena.offset(id) as u64;
2369 let len = arena.len_of(id) as u64;
2370 lo = lo.min(off);
2371 hi = hi.max(off.saturating_add(len));
2372 }
2373 if lo == u64::MAX {
2374 0
2375 } else {
2376 hi.saturating_sub(lo)
2377 }
2378}
2379
2380#[allow(dead_code)]
2381fn bind_two(
2382 device: &wgpu::Device,
2383 kernel: &Kernel,
2384 buf0: &wgpu::Buffer,
2385 buf1: &wgpu::Buffer,
2386) -> wgpu::BindGroup {
2387 let max_binding = device.limits().max_storage_buffer_binding_size;
2388 if buf0.size() > max_binding {
2389 panic!(
2390 "rlx-wgpu: bind_two buffer {} bytes exceeds max_storage_buffer_binding_size {}; \
2391 use bind_two_buf0_window or bind_op_output_window",
2392 buf0.size(),
2393 max_binding
2394 );
2395 }
2396 device.create_bind_group(&wgpu::BindGroupDescriptor {
2397 label: Some("rlx-wgpu bg"),
2398 layout: &kernel.bgl,
2399 entries: &[
2400 wgpu::BindGroupEntry {
2401 binding: 0,
2402 resource: buf0.as_entire_binding(),
2403 },
2404 wgpu::BindGroupEntry {
2405 binding: 1,
2406 resource: buf1.as_entire_binding(),
2407 },
2408 ],
2409 })
2410}
2411
2412fn bind_op_output_window(
2416 device: &wgpu::Device,
2417 kernel: &Kernel,
2418 arena: &Arena,
2419 out_id: NodeId,
2420 params: &wgpu::Buffer,
2421) -> wgpu::BindGroup {
2422 bind_op_window(device, kernel, arena, &[out_id], params)
2423}
2424
2425fn bind_op_window(
2426 device: &wgpu::Device,
2427 kernel: &Kernel,
2428 arena: &Arena,
2429 ids: &[NodeId],
2430 params: &wgpu::Buffer,
2431) -> wgpu::BindGroup {
2432 let max_binding = device.limits().max_storage_buffer_binding_size;
2433 let (base, size) = if arena_span_bytes(arena, ids) <= max_binding {
2434 arena_window_for_nodes(device, arena, ids)
2435 } else {
2436 arena_window_for_nodes(device, arena, &[ids[0]])
2437 };
2438 bind_two_buf0_window(device, kernel, &arena.buffer, base, size, params)
2439}
2440
2441pub(crate) fn aligned_bind_size(size: u64, base: u64, buffer_size: u64) -> Option<NonZeroU64> {
2446 let cap = (buffer_size & !3).saturating_sub(base);
2449 NonZeroU64::new(size.next_multiple_of(4).min(cap))
2450}
2451
2452fn bind_two_buf0_window(
2453 device: &wgpu::Device,
2454 kernel: &Kernel,
2455 buf0: &wgpu::Buffer,
2456 buf0_base: u64,
2457 buf0_size: u64,
2458 buf1: &wgpu::Buffer,
2459) -> wgpu::BindGroup {
2460 device.create_bind_group(&wgpu::BindGroupDescriptor {
2461 label: Some("rlx-wgpu bg window"),
2462 layout: &kernel.bgl,
2463 entries: &[
2464 wgpu::BindGroupEntry {
2465 binding: 0,
2466 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2467 buffer: buf0,
2468 offset: buf0_base,
2469 size: aligned_bind_size(buf0_size, buf0_base, buf0.size()),
2470 }),
2471 },
2472 wgpu::BindGroupEntry {
2473 binding: 1,
2474 resource: buf1.as_entire_binding(),
2475 },
2476 ],
2477 })
2478}
2479
2480fn derive_matmul_compute(
2503 dev: &wgpu::Device,
2504 graph: &Graph,
2505 mirror_acts: &HashSet<NodeId>,
2506 a_id: NodeId,
2507 b_id: NodeId,
2508 m: u32,
2509 k: u32,
2510 n: u32,
2511) -> MatmulCompute {
2512 if rlx_ir::env::flag("RLX_WGPU_MATMUL_F32_ONLY") {
2513 return MatmulCompute::F32;
2514 }
2515 use rlx_ir::DType;
2516 let a_dt = graph.node(a_id).shape.dtype();
2517 let b_dt = graph.node(b_id).shape.dtype();
2518 let any_low =
2519 matches!(a_dt, DType::F16 | DType::BF16) || matches!(b_dt, DType::F16 | DType::BF16);
2520 let coop16_aligned = m.is_multiple_of(32) && k.is_multiple_of(8) && n.is_multiple_of(32);
2530 let coop_f32_metal_aligned = k.is_multiple_of(8) && n.is_multiple_of(32);
2531 let coop_f32_portable_aligned = k.is_multiple_of(8) && n.is_multiple_of(8);
2532 let has_coop = dev
2533 .features()
2534 .contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX);
2535 let backend = crate::device::wgpu_device().map(|d| d.backend);
2536 if any_low
2541 && has_coop
2542 && dev.features().contains(wgpu::Features::SHADER_F16)
2543 && traces_to_param(graph, b_id)
2544 && coop16_aligned
2545 {
2546 return MatmulCompute::Coop16;
2547 }
2548 if !any_low && coop_f16_vk_eligible(dev, m, k, n) {
2549 if traces_to_param(graph, b_id)
2550 && !mirror_acts.contains(&a_id)
2551 && !mirror_acts.contains(&b_id)
2552 {
2553 return MatmulCompute::CoopF16Vk;
2554 }
2555 }
2556 let disabled = rlx_ir::env::flag("RLX_WGPU_NO_COOP_F32");
2565 let forced = rlx_ir::env::flag("RLX_WGPU_FORCE_COOP_F32");
2566 let metal_coop =
2573 !disabled && has_coop && coop_f32_metal_aligned && traces_to_param(graph, b_id) && forced;
2574 let _ = backend;
2575 let vulkan_coop = !disabled
2576 && has_coop
2577 && coop_f32_portable_aligned
2578 && traces_to_param(graph, b_id)
2579 && crate::device::coop_discrete_backend()
2580 && crate::device::coop_f32_8x8_supported();
2581 if metal_coop
2582 || vulkan_coop
2583 || (forced
2584 && has_coop
2585 && traces_to_param(graph, b_id)
2586 && (coop_f32_metal_aligned || coop_f32_portable_aligned))
2587 {
2588 return MatmulCompute::CoopF32;
2589 }
2590 MatmulCompute::F32
2591}
2592
2593#[allow(dead_code)]
2613fn detect_qkv_narrow_pattern(
2614 graph: &Graph,
2615 q_id: NodeId,
2616 k_id: NodeId,
2617 v_id: NodeId,
2618) -> Option<(NodeId, u32)> {
2619 let unwrap_narrow = |id: NodeId| -> Option<(NodeId, usize, usize, usize)> {
2620 let node = graph.node(id);
2621 match &node.op {
2622 Op::Narrow { axis, start, len } => Some((node.inputs[0], *axis, *start, *len)),
2623 _ => None,
2624 }
2625 };
2626 let (q_src, q_axis, q_start, q_len) = unwrap_narrow(q_id)?;
2627 let (k_src, k_axis, k_start, k_len) = unwrap_narrow(k_id)?;
2628 let (v_src, v_axis, v_start, v_len) = unwrap_narrow(v_id)?;
2629 if q_src != k_src || k_src != v_src {
2631 return None;
2632 }
2633 if q_len != k_len || k_len != v_len {
2635 return None;
2636 }
2637 if q_start != 0 || k_start != q_len || v_start != q_len * 2 {
2639 return None;
2640 }
2641 let src_rank = graph.node(q_src).shape.dims().len();
2643 if q_axis + 1 != src_rank || k_axis + 1 != src_rank || v_axis + 1 != src_rank {
2644 return None;
2645 }
2646 Some((q_src, q_len as u32))
2647}
2648
2649fn detect_residual_ln_tee_pattern(
2679 graph: &Graph,
2680) -> (
2681 HashMap<NodeId, (NodeId, NodeId, NodeId, NodeId, NodeId)>,
2682 HashSet<NodeId>,
2683) {
2684 use rlx_ir::op::BinaryOp;
2685 let mut consumers: HashMap<NodeId, usize> = HashMap::new();
2687 for node in graph.nodes() {
2688 for &input in &node.inputs {
2689 *consumers.entry(input).or_insert(0) += 1;
2690 }
2691 }
2692 for &out in &graph.outputs {
2693 *consumers.entry(out).or_insert(0) += 1;
2694 }
2695
2696 let mut ln_to_tee = HashMap::new();
2697 let mut skip_adds = HashSet::new();
2698 for node in graph.nodes() {
2699 let Op::LayerNorm { axis: _, eps: _ } = &node.op else {
2700 continue;
2701 };
2702 if node.inputs.len() < 3 {
2703 continue;
2704 } let in_id = node.inputs[0];
2706 let in_node = graph.node(in_id);
2707 if !matches!(in_node.op, Op::Binary(BinaryOp::Add)) {
2708 continue;
2709 }
2710 if consumers.get(&in_id).copied().unwrap_or(0) < 2 {
2713 continue;
2714 }
2715 if in_node.inputs.len() != 2 {
2718 continue;
2719 }
2720 let h_id = in_node.inputs[0];
2721 let delta_id = in_node.inputs[1];
2722 if graph.node(h_id).shape.dims() != node.shape.dims() {
2723 continue;
2724 }
2725 if graph.node(delta_id).shape.dims() != node.shape.dims() {
2726 continue;
2727 }
2728 let gamma_id = node.inputs[1];
2729 let beta_id = node.inputs[2];
2730 ln_to_tee.insert(node.id, (h_id, delta_id, gamma_id, beta_id, in_id));
2731 skip_adds.insert(in_id);
2732 }
2733 (ln_to_tee, skip_adds)
2734}
2735
2736fn detect_split_qkv_pattern(graph: &Graph) -> HashMap<NodeId, (NodeId, NodeId, NodeId)> {
2737 let mut consumers: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
2739 for node in graph.nodes() {
2740 for &input in &node.inputs {
2741 consumers.entry(input).or_default().push(node.id);
2742 }
2743 }
2744 for &out_id in &graph.outputs {
2747 consumers.entry(out_id).or_default().push(NodeId(u32::MAX));
2748 }
2749
2750 let mut result = HashMap::new();
2751 for node in graph.nodes() {
2752 if !matches!(node.op, Op::FusedMatMulBiasAct { activation: None }) {
2753 continue;
2754 }
2755 let cs = match consumers.get(&node.id) {
2756 Some(c) if c.len() == 3 => c,
2757 _ => continue,
2758 };
2759 let dims = node.shape.dims();
2760 if dims.is_empty() {
2761 continue;
2762 }
2763 let last_axis = dims.len() - 1;
2764 let n = dims[last_axis].unwrap_static();
2765 if n % 3 != 0 {
2766 continue;
2767 }
2768 let head_width = n / 3;
2769
2770 let mut narrows: Vec<(usize, NodeId)> = Vec::with_capacity(3);
2772 let mut all_match = true;
2773 for &c in cs {
2774 let cn = graph.node(c);
2775 match cn.op {
2776 Op::Narrow { axis, start, len }
2777 if axis == last_axis && len == head_width && cn.inputs[0] == node.id =>
2778 {
2779 narrows.push((start, c));
2780 }
2781 _ => {
2782 all_match = false;
2783 break;
2784 }
2785 }
2786 }
2787 if !all_match {
2788 continue;
2789 }
2790 narrows.sort_by_key(|&(start, _)| start);
2791 if narrows[0].0 != 0 || narrows[1].0 != head_width || narrows[2].0 != 2 * head_width {
2792 continue;
2793 }
2794 result.insert(node.id, (narrows[0].1, narrows[1].1, narrows[2].1));
2795 }
2796 result
2797}
2798
2799fn node_is_arena_param(param_offsets: &HashMap<String, NodeId>, id: NodeId) -> bool {
2805 param_offsets.values().any(|&nid| nid == id)
2806}
2807
2808fn traces_to_param(graph: &Graph, mut id: NodeId) -> bool {
2809 loop {
2810 let node = graph.node(id);
2811 match &node.op {
2812 Op::Param { .. } => return true,
2813 Op::Cast { .. } | Op::Reshape { .. } | Op::Transpose { .. } => {
2814 if node.inputs.is_empty() {
2815 return false;
2816 }
2817 id = node.inputs[0];
2818 }
2819 _ => return false,
2820 }
2821 }
2822}
2823
2824fn tensor_is_graph_param(
2825 graph: &Graph,
2826 param_offsets: &HashMap<String, NodeId>,
2827 id: NodeId,
2828) -> bool {
2829 node_is_arena_param(param_offsets, id) || traces_to_param(graph, id)
2830}
2831
2832fn traces_to_input(graph: &Graph, mut id: NodeId) -> bool {
2833 loop {
2834 let node = graph.node(id);
2835 match &node.op {
2836 Op::Input { .. } => return true,
2837 Op::Cast { .. } | Op::Reshape { .. } => {
2838 if node.inputs.is_empty() {
2839 return false;
2840 }
2841 id = node.inputs[0];
2842 }
2843 _ => return false,
2844 }
2845 }
2846}
2847
2848fn schedule_uses_coop_f16_vk(schedule: &[Step]) -> bool {
2851 schedule.iter().any(|s| {
2852 matches!(
2853 s,
2854 Step::Matmul {
2855 compute_precision: MatmulCompute::CoopF16Vk,
2856 ..
2857 } | Step::MatmulQkv {
2858 kind: MatmulQkvKind::CoopF16Vk,
2859 ..
2860 }
2861 )
2862 })
2863}
2864
2865fn register_coop_f16_vk_b_param(
2866 map: &mut HashMap<u32, String>,
2867 param_offsets: &HashMap<String, NodeId>,
2868 b_id: NodeId,
2869 b_off_f32: u32,
2870 compute: MatmulCompute,
2871) {
2872 if compute != MatmulCompute::CoopF16Vk {
2873 return;
2874 }
2875 for (name, &id) in param_offsets {
2876 if id == b_id {
2877 map.insert(b_off_f32, name.clone());
2878 return;
2879 }
2880 }
2881}
2882
2883fn tensor_host_name(
2884 input_offsets: &HashMap<String, NodeId>,
2885 param_offsets: &HashMap<String, NodeId>,
2886 id: NodeId,
2887) -> String {
2888 for (name, &nid) in input_offsets {
2889 if nid == id {
2890 return name.clone();
2891 }
2892 }
2893 for (name, &nid) in param_offsets {
2894 if nid == id {
2895 return name.clone();
2896 }
2897 }
2898 panic!("rlx-wgpu: CoopF16Vk host activation source {id} is not an input or param");
2899}
2900
2901fn host_tensor_f32<'a>(
2902 name: &str,
2903 inputs: &'a [(&str, &[f32])],
2904 stashed_params: &'a HashMap<String, Vec<f32>>,
2905) -> Option<&'a [f32]> {
2906 inputs
2907 .iter()
2908 .find(|(n, _)| *n == name)
2909 .map(|(_, d)| *d)
2910 .or_else(|| stashed_params.get(name).map(|v| v.as_slice()))
2911}
2912
2913fn apply_activation_host(act: Activation, data: &[f32]) -> Vec<f32> {
2914 data.iter()
2915 .map(|&x| match act {
2916 Activation::Relu => x.max(0.0),
2917 Activation::Sigmoid => 1.0 / (1.0 + (-x).exp()),
2918 Activation::Tanh => x.tanh(),
2919 Activation::Exp => x.exp(),
2920 Activation::Log => x.ln(),
2921 Activation::Sqrt => x.sqrt(),
2922 Activation::Rsqrt => 1.0 / x.sqrt(),
2923 Activation::Neg => -x,
2924 Activation::Abs => x.abs(),
2925 Activation::Gelu | Activation::GeluApprox => {
2926 let c = 0.797_884_6_f32;
2927 let x3 = x * x * x;
2928 let inner = (c * (x + 0.044_715 * x3)).clamp(-15.0, 15.0);
2929 0.5 * x * (1.0 + inner.tanh())
2930 }
2931 Activation::Silu => {
2932 let nx = (-x).clamp(-88.0, 88.0);
2933 x / (1.0 + nx.exp())
2934 }
2935 Activation::Round => x.round(),
2936 Activation::Sin => x.sin(),
2937 Activation::Cos => x.cos(),
2938 Activation::Tan => x.tan(),
2939 Activation::Atan => x.atan(),
2940 })
2941 .collect()
2942}
2943
2944fn collect_coop_f16_vk_mirror_activations(graph: &Graph, dev: &wgpu::Device) -> HashSet<NodeId> {
2946 let mut acts = HashSet::new();
2947 for node in graph.nodes() {
2948 if !matches!(node.op, Op::MatMul) {
2949 continue;
2950 }
2951 let a_id = node.inputs[0];
2952 let b_id = node.inputs[1];
2953 let a_shape = graph.node(a_id).shape.dims();
2954 let b_shape = graph.node(b_id).shape.dims();
2955 if a_shape.len() != 2 || b_shape.len() != 2 {
2956 continue;
2957 }
2958 let m = a_shape[0].unwrap_static() as u32;
2959 let k = a_shape[1].unwrap_static() as u32;
2960 let n = b_shape[1].unwrap_static() as u32;
2961 if !coop_f16_vk_eligible(dev, m, k, n) || !traces_to_param(graph, b_id) {
2962 continue;
2963 }
2964 if matches!(graph.node(a_id).op, Op::Activation(_)) {
2965 acts.insert(a_id);
2966 }
2967 if matches!(graph.node(b_id).op, Op::Activation(_)) {
2968 acts.insert(b_id);
2969 }
2970 }
2971 acts
2972}
2973
2974fn maybe_push_coop_f16_vk_casts(
2977 graph: &Graph,
2978 a_id: NodeId,
2979 b_id: NodeId,
2980 mirror_acts: &HashSet<NodeId>,
2981 device: &wgpu::Device,
2982 arena: &Arena,
2983 schedule: &mut Vec<Step>,
2984 uniforms: &mut Vec<wgpu::Buffer>,
2985 bind_groups: &mut Vec<wgpu::BindGroup>,
2986 mm_cast: &Option<&'static Kernel>,
2987 compute_precision: MatmulCompute,
2988 a_off_f32: u32,
2989 m: u32,
2990 k: u32,
2991 batch: u32,
2992 b_off_f32: u32,
2993 n: u32,
2994) {
2995 if compute_precision != MatmulCompute::CoopF16Vk {
2996 return;
2997 }
2998 let batch_n = batch.max(1);
2999 if !traces_to_input(graph, a_id)
3000 && !traces_to_param(graph, a_id)
3001 && !mirror_acts.contains(&a_id)
3002 {
3003 let a_elems = m.saturating_mul(k).saturating_mul(batch_n);
3004 let (base, size) = arena_window_for_nodes(device, arena, &[a_id]);
3005 push_cast_f32_to_f16_step(
3006 device,
3007 arena,
3008 base,
3009 size,
3010 schedule,
3011 uniforms,
3012 bind_groups,
3013 mm_cast,
3014 a_off_f32,
3015 a_elems,
3016 );
3017 }
3018 if !traces_to_input(graph, b_id)
3019 && !traces_to_param(graph, b_id)
3020 && !mirror_acts.contains(&b_id)
3021 {
3022 let b_elems = k.saturating_mul(n).saturating_mul(batch_n);
3023 let (base, size) = arena_window_for_nodes(device, arena, &[b_id]);
3024 push_cast_f32_to_f16_step(
3025 device,
3026 arena,
3027 base,
3028 size,
3029 schedule,
3030 uniforms,
3031 bind_groups,
3032 mm_cast,
3033 b_off_f32,
3034 b_elems,
3035 );
3036 }
3037}
3038
3039fn build_matmul_qkv_coop_f16_vk_bind_group(
3040 device: &wgpu::Device,
3041 mqk: &Kernel,
3042 arena: &Arena,
3043 arena_base: u64,
3044 arena_size: u64,
3045 params: &wgpu::Buffer,
3046 k: u32,
3047 n: u32,
3048 b_off: u32,
3049) -> (wgpu::BindGroup, u32) {
3050 let f16_buf = arena
3051 .f16_buffer
3052 .as_ref()
3053 .expect("CoopF16Vk QKV requires SHADER_F16 f16 shadow arena");
3054 let (f16_res, rebased_b) = {
3055 let (base, size, rebased) =
3056 f16_weight_bind_range(device, f16_buf.size(), b_off, k, n, 1, 0);
3057 (
3058 wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3059 buffer: f16_buf,
3060 offset: base,
3061 size: NonZeroU64::new(size),
3062 }),
3063 rebased,
3064 )
3065 };
3066 (
3067 device.create_bind_group(&wgpu::BindGroupDescriptor {
3068 label: Some("rlx-wgpu matmul_qkv_coop_f16_vk bg"),
3069 layout: &mqk.bgl,
3070 entries: &[
3071 wgpu::BindGroupEntry {
3072 binding: 0,
3073 resource: f16_res,
3074 },
3075 wgpu::BindGroupEntry {
3076 binding: 1,
3077 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3078 buffer: &arena.buffer,
3079 offset: arena_base,
3080 size: NonZeroU64::new(arena_size),
3081 }),
3082 },
3083 wgpu::BindGroupEntry {
3084 binding: 2,
3085 resource: params.as_entire_binding(),
3086 },
3087 ],
3088 }),
3089 rebased_b,
3090 )
3091}
3092fn push_cast_f32_to_f16_step(
3096 device: &wgpu::Device,
3097 arena: &Arena,
3098 arena_base: u64,
3099 arena_size: u64,
3100 schedule: &mut Vec<Step>,
3101 uniforms: &mut Vec<wgpu::Buffer>,
3102 bind_groups: &mut Vec<wgpu::BindGroup>,
3103 mm_cast: &Option<&'static Kernel>,
3104 src_off: u32,
3105 len: u32,
3106) {
3107 let kernel = match mm_cast {
3108 Some(k) => *k,
3109 None => return, };
3111 let f16_buf = match &arena.f16_buffer {
3112 Some(b) => b,
3113 None => return,
3114 };
3115 let p = CastF32ToF16Params {
3116 src_off: src_off.saturating_sub((arena_base / 4) as u32),
3117 len,
3118 _p0: 0,
3119 _p1: 0,
3120 };
3121 let u = device.create_buffer(&wgpu::BufferDescriptor {
3122 label: Some("rlx-wgpu cast_f32_to_f16 uniform"),
3123 size: std::mem::size_of::<CastF32ToF16Params>() as u64,
3124 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3125 mapped_at_creation: false,
3126 });
3127 let dev = wgpu_device().expect("rlx-wgpu: device gone");
3129 dev.queue.write_buffer(&u, 0, bytemuck::bytes_of(&p));
3130 let (f16_base, f16_size) = f16_shadow_bind_range(arena_base, arena_size, f16_buf.size());
3131 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
3132 label: Some("rlx-wgpu cast_f32_to_f16 bg"),
3133 layout: &kernel.bgl,
3134 entries: &[
3135 wgpu::BindGroupEntry {
3136 binding: 0,
3137 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3138 buffer: f16_buf,
3139 offset: f16_base,
3140 size: NonZeroU64::new(f16_size),
3141 }),
3142 },
3143 wgpu::BindGroupEntry {
3144 binding: 1,
3145 resource: u.as_entire_binding(),
3146 },
3147 wgpu::BindGroupEntry {
3148 binding: 2,
3149 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3150 buffer: &arena.buffer,
3151 offset: arena_base,
3152 size: NonZeroU64::new(arena_size),
3153 }),
3154 },
3155 ],
3156 });
3157 schedule.push(Step::CastF32ToF16 { params: p });
3158 uniforms.push(u);
3159 bind_groups.push(bg);
3160}
3161
3162fn build_matmul_bind_group(
3166 device: &wgpu::Device,
3167 mm_k: &Kernel,
3168 _mm_w: &Kernel,
3169 mm_f16w: &Option<&'static Kernel>,
3170 mm_f16c: &Option<&'static Kernel>,
3171 mm_coop: &Option<&'static Kernel>,
3172 mm_coop_f32: &Option<&'static Kernel>,
3173 arena: &Arena,
3174 arena_base: u64,
3175 arena_size: u64,
3176 params: &wgpu::Buffer,
3177 b_is_param: bool,
3178 compute_precision: MatmulCompute,
3179 k: u32,
3180 n: u32,
3181 batch: u32,
3182 b_off: u32,
3183 b_batch_stride: u32,
3184) -> (wgpu::BindGroup, u32) {
3185 let f16_bind = |b_off: u32| -> (wgpu::BindingResource<'_>, u32) {
3186 let f16_buf = arena
3187 .f16_buffer
3188 .as_ref()
3189 .expect("f16 weight bind without f16_buffer");
3190 let (base, size, rebased) =
3191 f16_weight_bind_range(device, f16_buf.size(), b_off, k, n, batch, b_batch_stride);
3192 (
3193 wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3194 buffer: f16_buf,
3195 offset: base,
3196 size: NonZeroU64::new(size),
3197 }),
3198 rebased,
3199 )
3200 };
3201 if compute_precision == MatmulCompute::CoopF16Vk
3202 && let (Some(coop_vk), Some(_f16_buf)) =
3203 (matmul_coop_f16_vulkan_kernel(device), &arena.f16_buffer)
3204 {
3205 let (f16_res, rebased_b) = f16_bind(b_off);
3206 return (
3207 device.create_bind_group(&wgpu::BindGroupDescriptor {
3208 label: Some("rlx-wgpu matmul_coop_f16_vulkan bg"),
3209 layout: &coop_vk.bgl,
3210 entries: &[
3211 wgpu::BindGroupEntry {
3212 binding: 0,
3213 resource: f16_res,
3214 },
3215 wgpu::BindGroupEntry {
3216 binding: 1,
3217 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3218 buffer: &arena.buffer,
3219 offset: arena_base,
3220 size: NonZeroU64::new(arena_size),
3221 }),
3222 },
3223 wgpu::BindGroupEntry {
3224 binding: 2,
3225 resource: params.as_entire_binding(),
3226 },
3227 ],
3228 }),
3229 rebased_b,
3230 );
3231 }
3232 if b_is_param
3233 && compute_precision == MatmulCompute::CoopF32
3234 && let Some(coop_f32) = mm_coop_f32
3235 {
3236 return (
3239 device.create_bind_group(&wgpu::BindGroupDescriptor {
3240 label: Some("rlx-wgpu matmul_coop_f32 bg"),
3241 layout: &coop_f32.bgl,
3242 entries: &[
3243 wgpu::BindGroupEntry {
3244 binding: 0,
3245 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3246 buffer: &arena.buffer,
3247 offset: arena_base,
3248 size: NonZeroU64::new(arena_size),
3249 }),
3250 },
3251 wgpu::BindGroupEntry {
3252 binding: 1,
3253 resource: params.as_entire_binding(),
3254 },
3255 ],
3256 }),
3257 b_off,
3258 );
3259 }
3260 if b_is_param
3261 && compute_precision == MatmulCompute::Coop16
3262 && let (Some(_f16_buf), Some(coop)) = (&arena.f16_buffer, mm_coop)
3263 {
3264 let (f16_res, rebased_b) = f16_bind(b_off);
3265 return (
3269 device.create_bind_group(&wgpu::BindGroupDescriptor {
3270 label: Some("rlx-wgpu matmul_coop16 bg"),
3271 layout: &coop.bgl,
3272 entries: &[
3273 wgpu::BindGroupEntry {
3274 binding: 0,
3275 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3276 buffer: &arena.buffer,
3277 offset: arena_base,
3278 size: NonZeroU64::new(arena_size),
3279 }),
3280 },
3281 wgpu::BindGroupEntry {
3282 binding: 1,
3283 resource: params.as_entire_binding(),
3284 },
3285 wgpu::BindGroupEntry {
3286 binding: 2,
3287 resource: f16_res,
3288 }, ],
3290 }),
3291 rebased_b,
3292 );
3293 }
3294 if b_is_param
3295 && compute_precision == MatmulCompute::F16
3296 && let (Some(_f16_buf), Some(f16c)) = (&arena.f16_buffer, mm_f16c)
3297 {
3298 let (f16_res, rebased_b) = f16_bind(b_off);
3299 return (
3300 device.create_bind_group(&wgpu::BindGroupDescriptor {
3301 label: Some("rlx-wgpu matmul_f16_compute bg"),
3302 layout: &f16c.bgl,
3303 entries: &[
3304 wgpu::BindGroupEntry {
3305 binding: 0,
3306 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3307 buffer: &arena.buffer,
3308 offset: arena_base,
3309 size: NonZeroU64::new(arena_size),
3310 }),
3311 },
3312 wgpu::BindGroupEntry {
3313 binding: 1,
3314 resource: params.as_entire_binding(),
3315 },
3316 wgpu::BindGroupEntry {
3317 binding: 2,
3318 resource: f16_res,
3319 },
3320 ],
3321 }),
3322 rebased_b,
3323 );
3324 }
3325 let f16w_opt_in = rlx_ir::env::flag("RLX_WGPU_F16_WEIGHTS");
3326 if b_is_param
3327 && f16w_opt_in
3328 && let (Some(_f16_buf), Some(f16w)) = (&arena.f16_buffer, mm_f16w)
3329 {
3330 let (f16_res, rebased_b) = f16_bind(b_off);
3331 return (
3332 device.create_bind_group(&wgpu::BindGroupDescriptor {
3333 label: Some("rlx-wgpu matmul_f16w bg"),
3334 layout: &f16w.bgl,
3335 entries: &[
3336 wgpu::BindGroupEntry {
3337 binding: 0,
3338 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3339 buffer: &arena.buffer,
3340 offset: arena_base,
3341 size: NonZeroU64::new(arena_size),
3342 }),
3343 },
3344 wgpu::BindGroupEntry {
3345 binding: 1,
3346 resource: params.as_entire_binding(),
3347 },
3348 wgpu::BindGroupEntry {
3349 binding: 2,
3350 resource: f16_res,
3351 },
3352 ],
3353 }),
3354 rebased_b,
3355 );
3356 }
3357 (
3358 bind_two_buf0_window(device, mm_k, &arena.buffer, arena_base, arena_size, params),
3359 b_off,
3360 )
3361}