1use std::collections::{HashMap, HashSet};
23use std::num::NonZeroU64;
24
25use rlx_ir::dynamic::{bind_graph, has_dynamic_dims, infer_bindings_from_f32_inputs, same_binding};
26use rlx_ir::op::{Activation, BinaryOp, CmpOp, MaskKind, ReduceOp};
27use rlx_ir::shape::DimBinding;
28use rlx_ir::{Graph, NodeId, Op};
29
30use crate::buffer::{
31 Arena, ReadbackLayout, ReadbackStaging, TinyReadbackStaging, decode_mapped_readback_f32,
32 decode_tiny_mapped_f32, encode_readback_copies, plan_f32_uniform, read_f32_many_pooled,
33 schedule_readback_map, use_tiny_readback, wait_readback_map,
34};
35use crate::device::wgpu_device;
36use crate::kernels::{
37 ArgmaxParams, AttentionBwdParams, AttentionParams, BatchElementwiseRegionParams, BinaryParams,
38 Conv1dParams, Conv2dParams, Conv3dParams, CopyParams, CumsumBwdParams, CumsumParams,
39 DequantMatmulParams, ElementwiseRegionParams, ExpandParams, FmaParams, FusedResidualLnParams,
40 FusedResidualLnTeeParams, FusedResidualRmsNormParams, GatherAxisParams, GatherBwdParams,
41 GatherParams, GroupedMatmulParams, GruParams, Kernel, LayerNormBwdParams, LayerNormParams,
42 Mamba2Params, MatmulParams, MatmulQkvParams, NarrowConcatParams, Pool1dParams, Pool2dParams,
43 Pool3dParams, ReduceParams, RmsNormBwdParams, RnnParams, RopeBwdParams, RopeParams,
44 SampleParams, ScatterAddParams, SceParams, SelectiveScanParams, SoftmaxParams, TopKParams,
45 TransposeParams, UmapKnnParams, UnaryParams, WelchPeaksGpuParams, WhereParams, argmax_kernel,
46 attention_bwd_kernel, attention_kernel, batch_elementwise_region_kernel, binary_kernel,
47 cast_f32_to_f16_kernel, compare_kernel, concat_kernel, conv1d_kernel, conv2d_kernel,
48 conv3d_kernel, copy_kernel, cumsum_backward_kernel, cumsum_kernel, dequant_matmul_kernel,
49 elementwise_region_kernel, elementwise_region_spatial_kernel, expand_kernel, fma_kernel,
50 fused_residual_ln_kernel, fused_residual_ln_tee_kernel, fused_residual_rms_norm_kernel,
51 gather_axis_kernel, gather_backward_acc_kernel, gather_backward_zero_kernel, gather_kernel,
52 gather_split_kernel, grouped_matmul_kernel, gru_kernel,
53 layer_norm_backward_gamma_partial_kernel, layer_norm_backward_gamma_reduce_kernel,
54 layer_norm_backward_input_kernel, layernorm_kernel, mamba2_kernel,
55 matmul_coop_f16_vulkan_active_kernel, matmul_coop_f16_vulkan_kernel,
56 matmul_coop_f32_active_kernel, matmul_coop16_kernel, matmul_f16_compute_kernel,
57 matmul_f16w_kernel, matmul_kernel, matmul_qkv_coop_f16_vk_active_kernel,
58 matmul_qkv_coop_f16_vk_kernel, matmul_qkv_coop_f32_kernel, matmul_qkv_kernel,
59 matmul_wide_active_kernel, matmul_wide_kernel, narrow_kernel, pool1d_kernel, pool2d_kernel,
60 pool3d_kernel, reduce_kernel, rms_norm_backward_kernel, rms_norm_backward_param_kernel,
61 rnn_kernel, rope_backward_kernel, rope_kernel, sample_kernel, scatter_add_kernel,
62 selective_scan_kernel, softmax_cross_entropy_kernel, softmax_kernel, topk_kernel,
63 transpose_kernel, umap_knn_kernel, unary_f16_mirror_kernel, unary_kernel,
64 welch_peaks_gpu_kernel, where_kernel,
65};
66fn compute_scratch_bytes(graph: &rlx_ir::Graph) -> usize {
70 const ROWS_PER_WG: u32 = 16;
71 let mut max_bytes = 0usize;
72 for node in graph.nodes() {
73 if matches!(
79 &node.op,
80 rlx_ir::Op::LayerNorm { .. } | rlx_ir::Op::RmsNorm { .. }
81 ) {
82 let x_shape = &graph.node(node.inputs[0]).shape;
83 let h_dim = x_shape.dim(x_shape.rank() - 1);
84 if h_dim.is_static() {
85 let h = h_dim.unwrap_static();
86 let bytes = ((h * 4).div_ceil(256) * 256) * 2;
88 if bytes > max_bytes {
89 max_bytes = bytes;
90 }
91 }
92 }
93 if let rlx_ir::Op::LayerNormBackwardGamma { .. } = &node.op {
94 let x_shape = &graph.node(node.inputs[0]).shape;
95 let Some(elems) = x_shape.num_elements() else {
96 continue;
97 };
98 let h_dim = x_shape.dim(x_shape.rank() - 1);
99 if !h_dim.is_static() {
100 continue;
101 }
102 let h = h_dim.unwrap_static();
103 if h == 0 {
104 continue;
105 }
106 let rows = (elems / h) as u32;
107 let num_workgroups = rows.div_ceil(ROWS_PER_WG.max(1));
108 let bytes = (num_workgroups as usize) * h * 4;
109 if bytes > max_bytes {
110 max_bytes = bytes;
111 }
112 }
113 }
114 max_bytes.max(64 * 1024 * 1024)
118}
119
120fn hash_f32_input(data: &[f32]) -> u64 {
123 let bytes = bytemuck::cast_slice(data);
124 let mut h: u64 = 0xcbf29ce484222325;
125 h ^= data.len() as u64;
126 h = h.wrapping_mul(0x100000001b3);
127 for chunk in bytes.chunks(8) {
128 let mut arr = [0u8; 8];
129 arr[..chunk.len()].copy_from_slice(chunk);
130 h ^= u64::from_le_bytes(arr);
131 h = h.wrapping_mul(0x100000001b3);
132 }
133 h
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147enum MatmulCompute {
148 F32,
149 F16,
150 Coop16,
151 CoopF32,
156 CoopF16Vk,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163enum MatmulQkvKind {
164 F32,
165 CoopF32,
166 CoopF16Vk,
167}
168
169#[allow(dead_code)]
179#[derive(Debug, Clone, Copy)]
180struct CastF32ToF16Params {
181 pub src_off: u32, pub len: u32,
183 pub _p0: u32,
184 pub _p1: u32,
185}
186unsafe impl bytemuck::Pod for CastF32ToF16Params {}
187unsafe impl bytemuck::Zeroable for CastF32ToF16Params {}
188
189#[allow(dead_code)]
198enum Step {
199 CastF32ToF16 {
200 params: CastF32ToF16Params,
201 },
202 Matmul {
203 m: u32,
204 k: u32,
205 n: u32,
206 a_off_f32: u32,
207 b_off_f32: u32,
208 c_off_f32: u32,
209 batch: u32,
210 a_batch_stride: u32,
211 b_batch_stride: u32,
212 c_batch_stride: u32,
213 has_bias: u32,
214 bias_off_f32: u32,
215 act_id: u32, b_is_param: bool,
222 compute_precision: MatmulCompute,
228 },
229 Binary {
230 params: BinaryParams,
231 },
232 Compare {
233 params: BinaryParams,
234 },
235 Unary {
236 params: UnaryParams,
237 f16_mirror: bool,
238 },
239 Where {
240 params: WhereParams,
241 },
242 Fma {
243 params: FmaParams,
244 },
245 Reduce {
246 params: ReduceParams,
247 },
248 Softmax {
249 params: SoftmaxParams,
250 },
251 SoftmaxCrossEntropy {
252 params: SceParams,
253 },
254 LayerNorm {
255 params: LayerNormParams,
256 },
257 Cumsum {
258 params: CumsumParams,
259 },
260 FftGpu {
262 src_off: u32,
263 dst_off: u32,
264 outer: u32,
265 n: u32,
266 inverse: u32,
267 norm_scale: f32,
268 },
269 FftHost {
272 src_byte_off: u32,
273 dst_byte_off: u32,
274 outer: u32,
275 n_complex: u32,
276 inverse: bool,
277 norm_tag: u32,
278 dtype_tag: u32,
279 },
280 WelchPeaksHost {
282 spec_byte_off: u32,
283 dst_byte_off: u32,
284 welch_batch: u32,
285 n_fft: u32,
286 n_segments: u32,
287 k: u32,
288 },
289 LogMelHost {
290 spec_byte_off: u32,
291 filt_byte_off: u32,
292 dst_byte_off: u32,
293 outer: u32,
294 n_fft: u32,
295 n_bins: u32,
296 n_mels: u32,
297 },
298 LogMelBackwardHost {
299 spec_byte_off: u32,
300 filt_byte_off: u32,
301 dy_byte_off: u32,
302 dst_byte_off: u32,
303 outer: u32,
304 n_fft: u32,
305 n_bins: u32,
306 n_mels: u32,
307 },
308 Im2ColHost {
310 x_byte_off: u32,
311 col_byte_off: u32,
312 n: u32,
313 c_in: u32,
314 h: u32,
315 w: u32,
316 h_out: u32,
317 w_out: u32,
318 kh: u32,
319 kw: u32,
320 sh: u32,
321 sw: u32,
322 ph: u32,
323 pw: u32,
324 dh: u32,
325 dw_dil: u32,
326 },
327 RngNormalHost {
329 dst_byte_off: u32,
330 len: u32,
331 mean: f32,
332 scale: f32,
333 key: u64,
334 op_seed: Option<f32>,
335 },
336 RngUniformHost {
338 dst_byte_off: u32,
339 len: u32,
340 low: f32,
341 high: f32,
342 key: u64,
343 op_seed: Option<f32>,
344 },
345 BufferCopy {
349 src_byte_off: u64,
352 dst_byte_off: u64,
353 bytes: u32,
354 },
355 Copy {
356 params: CopyParams,
357 },
358 ElementwiseRegion {
364 params: ElementwiseRegionParams,
365 },
366 BatchElementwiseRegion {
367 params: BatchElementwiseRegionParams,
368 },
369 Transpose {
370 params: TransposeParams,
371 meta_idx: usize,
372 },
373 Narrow {
374 params: NarrowConcatParams,
375 },
376 Concat {
377 params: NarrowConcatParams,
378 }, Gather {
380 params: GatherParams,
381 },
382 GatherAxis {
383 params: GatherAxisParams,
384 },
385 Attention {
386 params: AttentionParams,
387 mask_buf: Option<wgpu::Buffer>,
388 },
389 AttentionBackward {
390 params: AttentionBwdParams,
391 mask_buf: Option<wgpu::Buffer>,
392 },
393 Rope {
394 params: RopeParams,
395 },
396 Expand {
397 params: ExpandParams,
398 meta_idx: usize,
399 },
400 Argmax {
401 params: ArgmaxParams,
402 },
403 Pool2d {
404 params: Pool2dParams,
405 },
406 Conv2d {
407 params: Conv2dParams,
408 },
409 Pool1d {
410 params: Pool1dParams,
411 },
412 Pool3d {
413 params: Pool3dParams,
414 },
415 Conv1d {
416 params: Conv1dParams,
417 },
418 Conv3d {
419 params: Conv3dParams,
420 },
421 ScatterAdd {
422 params: ScatterAddParams,
423 },
424 TopK {
425 params: TopKParams,
426 },
427 WelchPeaksGpu {
428 params: WelchPeaksGpuParams,
429 },
430 GroupedMatmul {
431 params: GroupedMatmulParams,
432 },
433 Sample {
434 params: SampleParams,
435 },
436 SelectiveScan {
437 params: SelectiveScanParams,
438 },
439 Mamba2 {
441 params: Mamba2Params,
442 },
443 Gru {
445 params: GruParams,
446 },
447 Rnn {
449 params: RnnParams,
450 },
451 GruHost {
453 x: u32,
454 w_ih: u32,
455 w_hh: u32,
456 b_ih: u32,
457 b_hh: u32,
458 h0: u32,
459 dst: u32,
460 batch: u32,
461 seq: u32,
462 input_size: u32,
463 hidden: u32,
464 num_layers: u32,
465 bidirectional: bool,
466 carry: bool,
467 },
468 RnnHost {
470 x: u32,
471 w_ih: u32,
472 w_hh: u32,
473 bias: u32,
474 h0: u32,
475 dst: u32,
476 batch: u32,
477 seq: u32,
478 input_size: u32,
479 hidden: u32,
480 num_layers: u32,
481 bidirectional: bool,
482 carry: bool,
483 relu: bool,
484 },
485 DequantMatmul {
486 params: DequantMatmulParams,
487 },
488 GatherSplit {
494 n_out: u32,
495 n_idx: u32,
496 dim: u32,
497 vocab: u32,
498 table_byte_off: u64,
499 idx_byte_off: u64,
500 out_byte_off: u64,
501 },
502 DequantMatmulGguf {
504 m: u32,
505 k: u32,
506 n: u32,
507 scheme_id: u32,
508 x_byte_off: u64,
512 w_byte_off: u64,
513 out_byte_off: u64,
514 },
515 DequantGroupedMatmulGguf {
517 m: u32,
518 k: u32,
519 n: u32,
520 num_experts: u32,
521 scheme_id: u32,
522 x_byte_off: u64,
523 w_byte_off: u64,
524 idx_byte_off: u64,
525 out_byte_off: u64,
526 },
527 GatedDeltaNet {
529 q_byte_off: u32,
530 k_byte_off: u32,
531 v_byte_off: u32,
532 g_byte_off: u32,
533 beta_byte_off: u32,
534 state_byte_off: u32,
535 dst_byte_off: u32,
536 batch: u32,
537 seq: u32,
538 heads: u32,
539 state_size: u32,
540 use_carry: bool,
541 },
542 Lstm {
543 x_byte_off: u32,
544 w_ih_byte_off: u32,
545 w_hh_byte_off: u32,
546 bias_byte_off: u32,
547 h0_byte_off: u32,
548 c0_byte_off: u32,
549 dst_byte_off: u32,
550 batch: u32,
551 seq: u32,
552 input_size: u32,
553 hidden: u32,
554 num_layers: u32,
555 bidirectional: bool,
556 carry: bool,
557 },
558 ConvTranspose2d {
559 src_byte_off: u32,
560 weight_byte_off: u32,
561 dst_byte_off: u32,
562 n: u32,
563 c_in: u32,
564 h: u32,
565 w_in: u32,
566 c_out: u32,
567 h_out: u32,
568 w_out: u32,
569 kh: u32,
570 kw: u32,
571 sh: u32,
572 sw: u32,
573 ph: u32,
574 pw: u32,
575 dh: u32,
576 dw: u32,
577 groups: u32,
578 },
579 GroupNormHost {
581 src_byte_off: u32,
582 gamma_byte_off: u32,
583 beta_byte_off: u32,
584 dst_byte_off: u32,
585 n: u32,
586 c: u32,
587 h: u32,
588 w: u32,
589 num_groups: u32,
590 eps: f32,
591 },
592 LayerNorm2dHost {
594 src_byte_off: u32,
595 gamma_byte_off: u32,
596 beta_byte_off: u32,
597 dst_byte_off: u32,
598 n: u32,
599 c: u32,
600 h: u32,
601 w: u32,
602 eps: f32,
603 },
604 ResizeNearest2xHost {
606 src_byte_off: u32,
607 dst_byte_off: u32,
608 n: u32,
609 c: u32,
610 h: u32,
611 w: u32,
612 },
613 ReverseHost {
615 src_byte_off: u32,
616 dst_byte_off: u32,
617 dims: Vec<u32>,
618 rev_mask: Vec<bool>,
619 elem_bytes: u32,
620 },
621 ArgReduceHost {
623 src_byte_off: u32,
624 dst_byte_off: u32,
625 outer: u32,
626 reduced: u32,
627 inner: u32,
628 is_max: bool,
629 },
630 Llada2GroupLimitedGate {
631 sig_byte_off: u32,
632 route_byte_off: u32,
633 out_byte_off: u32,
634 n_elems: u32,
635 attrs: [u8; 20],
636 },
637 UmapKnn {
638 params: UmapKnnParams,
639 },
640 UmapKnnHost {
642 pairwise_byte_off: u32,
643 out_byte_off: u32,
644 n: u32,
645 k: u32,
646 },
647 MsDeformAttnHost {
649 in_offs: Vec<(u32, u32)>, out_byte_off: u32,
651 out_bytes: u32,
652 attrs: Vec<u8>,
653 },
654 #[cfg(feature = "splat")]
656 GaussianSplatRender {
657 positions_byte_off: u32,
658 positions_len: u32,
659 scales_byte_off: u32,
660 scales_len: u32,
661 rotations_byte_off: u32,
662 rotations_len: u32,
663 opacities_byte_off: u32,
664 opacities_len: u32,
665 colors_byte_off: u32,
666 colors_len: u32,
667 sh_coeffs_byte_off: u32,
668 sh_coeffs_len: u32,
669 meta_byte_off: u32,
670 dst_byte_off: u32,
671 dst_len: u32,
672 width: u32,
673 height: u32,
674 tile_size: u32,
675 radius_scale: f32,
676 alpha_cutoff: f32,
677 max_splat_steps: u32,
678 transmittance_threshold: f32,
679 max_list_entries: u32,
680 },
681 #[cfg(feature = "splat")]
683 GaussianSplatRenderBackward {
684 positions_byte_off: u32,
685 positions_len: u32,
686 scales_byte_off: u32,
687 scales_len: u32,
688 rotations_byte_off: u32,
689 rotations_len: u32,
690 opacities_byte_off: u32,
691 opacities_len: u32,
692 colors_byte_off: u32,
693 colors_len: u32,
694 sh_coeffs_byte_off: u32,
695 sh_coeffs_len: u32,
696 meta_byte_off: u32,
697 d_loss_byte_off: u32,
698 d_loss_len: u32,
699 packed_byte_off: u32,
700 packed_len: u32,
701 width: u32,
702 height: u32,
703 tile_size: u32,
704 radius_scale: f32,
705 alpha_cutoff: f32,
706 max_splat_steps: u32,
707 transmittance_threshold: f32,
708 max_list_entries: u32,
709 loss_grad_clip: f32,
710 sh_band: u32,
711 max_anisotropy: f32,
712 },
713 #[cfg(feature = "splat")]
714 GaussianSplatPrepare {
715 positions_byte_off: u32,
716 positions_len: u32,
717 scales_byte_off: u32,
718 scales_len: u32,
719 rotations_byte_off: u32,
720 rotations_len: u32,
721 opacities_byte_off: u32,
722 opacities_len: u32,
723 colors_byte_off: u32,
724 colors_len: u32,
725 sh_coeffs_byte_off: u32,
726 sh_coeffs_len: u32,
727 meta_byte_off: u32,
728 meta_len: u32,
729 prep_byte_off: u32,
730 prep_len: u32,
731 width: u32,
732 height: u32,
733 tile_size: u32,
734 radius_scale: f32,
735 alpha_cutoff: f32,
736 max_splat_steps: u32,
737 transmittance_threshold: f32,
738 max_list_entries: u32,
739 },
740 #[cfg(feature = "splat")]
741 GaussianSplatRasterize {
742 prep_byte_off: u32,
743 prep_len: u32,
744 meta_byte_off: u32,
745 meta_len: u32,
746 dst_byte_off: u32,
747 dst_len: u32,
748 count: u32,
749 width: u32,
750 height: u32,
751 tile_size: u32,
752 alpha_cutoff: f32,
753 max_splat_steps: u32,
754 transmittance_threshold: f32,
755 max_list_entries: u32,
756 },
757 RmsNormBackwardInput {
758 params: RmsNormBwdParams,
759 },
760 RmsNormBackwardGamma {
761 params: RmsNormBwdParams,
762 },
763 RmsNormBackwardBeta {
764 params: RmsNormBwdParams,
765 },
766 LayerNormBackwardInput {
767 params: LayerNormBwdParams,
768 },
769 LayerNormBackwardGammaPartial {
770 params: LayerNormBwdParams,
771 num_workgroups: u32,
772 },
773 LayerNormBackwardGammaReduce {
774 params: LayerNormBwdParams,
775 },
776 RopeBackward {
777 params: RopeBwdParams,
778 },
779 CumsumBackward {
780 params: CumsumBwdParams,
781 },
782 GatherBackward {
783 params: GatherBwdParams,
784 },
785 FusedResidualLn {
786 params: FusedResidualLnParams,
787 },
788 MatmulQkv {
793 params: MatmulQkvParams,
794 kind: MatmulQkvKind,
795 },
796 FusedResidualLnTee {
800 params: FusedResidualLnTeeParams,
801 },
802 FusedResidualRmsNorm {
803 params: FusedResidualRmsNormParams,
804 },
805}
806
807pub struct WgpuExecutable {
808 graph: Graph,
809 arena: Arena,
810 dequant_scratch_off: usize,
812 schedule: Vec<Step>,
813 input_offsets: HashMap<String, NodeId>,
814 param_offsets: HashMap<String, NodeId>,
815 uniforms: Vec<wgpu::Buffer>,
818 bind_groups: Vec<wgpu::BindGroup>,
819 meta_buffers: Vec<wgpu::Buffer>,
822
823 unresolved: Option<Graph>,
830 last_binding: Option<DimBinding>,
831 pending_params: HashMap<String, Vec<f32>>,
835 pending_param_bytes: HashMap<String, Vec<u8>>,
836 pub(crate) active_extent: Option<(usize, usize)>,
840 uniforms_active_extent: Option<Option<(usize, usize)>>,
850 input_staging_hashes: HashMap<String, u64>,
852 coop_f16_vk: bool,
855 coop_f16_b_param: HashMap<u32, String>,
857 coop_f16_vk_wide_b: HashSet<String>,
859 coop_f16_vk_wide_bind_groups: HashMap<usize, wgpu::BindGroup>,
861 coop_f16_host_activations: Vec<(NodeId, Activation, String)>,
863 stashed_params: HashMap<String, Vec<f32>>,
865 readback_staging: Option<ReadbackStaging>,
867 tiny_readback: Option<TinyReadbackStaging>,
869 dispatch_only: bool,
874 fft_gpu_steps: Vec<crate::fft_dispatch::FftGpuResources>,
876 gpu_handles: HashMap<String, Vec<f32>>,
878 gpu_handle_feeds: HashMap<String, usize>,
879 gpu_handle_resident: HashSet<String>,
881 pending_read_indices: Option<Vec<usize>>,
882 rng: std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
884}
885
886impl Step {
887 pub fn safe_for_active_extent(&self) -> bool {
893 match self {
894 Step::Binary { .. }
895 | Step::Compare { .. }
896 | Step::Unary { .. }
897 | Step::Where { .. }
898 | Step::Fma { .. }
899 | Step::Reduce { .. }
900 | Step::Softmax { .. }
901 | Step::SoftmaxCrossEntropy { .. }
902 | Step::LayerNorm { .. }
903 | Step::FusedResidualLn { .. }
904 | Step::FusedResidualLnTee { .. }
905 | Step::FusedResidualRmsNorm { .. }
906 | Step::Cumsum { .. }
907 | Step::Copy { .. }
908 | Step::ElementwiseRegion { .. }
909 | Step::BatchElementwiseRegion { .. }
910 | Step::Argmax { .. }
911 | Step::TopK { .. }
912 | Step::WelchPeaksGpu { .. }
913 | Step::Sample { .. }
914 | Step::Gather { .. }
915 | Step::GatherAxis { .. }
916 | Step::GatherSplit { .. }
917 | Step::GroupedMatmul { .. }
918 | Step::DequantMatmul { .. }
919 | Step::DequantMatmulGguf { .. }
920 | Step::DequantGroupedMatmulGguf { .. }
921 | Step::GatedDeltaNet { .. }
922 | Step::Lstm { .. }
923 | Step::ConvTranspose2d { .. }
924 | Step::GroupNormHost { .. }
925 | Step::LayerNorm2dHost { .. }
926 | Step::ResizeNearest2xHost { .. }
927 | Step::ReverseHost { .. }
928 | Step::ArgReduceHost { .. }
929 | Step::GruHost { .. }
930 | Step::RnnHost { .. }
931 | Step::Llada2GroupLimitedGate { .. }
932 | Step::UmapKnn { .. }
933 | Step::UmapKnnHost { .. }
934 | Step::MsDeformAttnHost { .. }
935 | Step::Conv1d { .. }
936 | Step::Conv2d { .. }
937 | Step::Conv3d { .. }
938 | Step::Pool1d { .. }
939 | Step::Pool2d { .. }
940 | Step::Pool3d { .. }
941 | Step::ScatterAdd { .. }
942 | Step::BufferCopy { .. } => true,
943 Step::FftGpu { .. } | Step::FftHost { .. } => true,
948 Step::Im2ColHost { .. }
949 | Step::RngNormalHost { .. }
950 | Step::RngUniformHost { .. }
951 | Step::WelchPeaksHost { .. }
952 | Step::LogMelHost { .. }
953 | Step::LogMelBackwardHost { .. } => true,
954 Step::Matmul { .. } => true,
959 Step::MatmulQkv { .. } => true,
963 Step::CastF32ToF16 { .. } => true,
964 Step::Attention { .. } => true,
970 Step::AttentionBackward { .. } => true,
971 Step::SelectiveScan { .. } => true,
976 Step::Mamba2 { .. } => true,
978 Step::Gru { .. } => true,
981 Step::Rnn { .. } => true,
982 Step::Narrow { .. } => true,
991 Step::Concat { .. } => true,
992 Step::Rope { .. } => true,
998 Step::Transpose { params, .. } => params.bucket_outermost == 1,
1004 Step::Expand { params, .. } => params.bucket_outermost == 1,
1008 Step::RmsNormBackwardInput { .. }
1011 | Step::RmsNormBackwardGamma { .. }
1012 | Step::RmsNormBackwardBeta { .. }
1013 | Step::LayerNormBackwardInput { .. }
1014 | Step::LayerNormBackwardGammaPartial { .. }
1015 | Step::LayerNormBackwardGammaReduce { .. }
1016 | Step::RopeBackward { .. }
1017 | Step::CumsumBackward { .. }
1018 | Step::GatherBackward { .. } => false,
1019 #[cfg(feature = "splat")]
1020 Step::GaussianSplatRender { .. }
1021 | Step::GaussianSplatRenderBackward { .. }
1022 | Step::GaussianSplatPrepare { .. }
1023 | Step::GaussianSplatRasterize { .. } => false,
1024 }
1025 }
1026}
1027
1028fn fft_dtype_tag(dtype: rlx_ir::DType) -> u32 {
1031 match dtype {
1032 rlx_ir::DType::F32 => 0,
1033 rlx_ir::DType::F64 => 1,
1034 rlx_ir::DType::C64 => 2,
1035 other => panic!("rlx-wgpu Op::Fft: unsupported dtype {other:?}"),
1036 }
1037}
1038
1039fn fft_dtype_from_tag(tag: u32) -> rlx_ir::DType {
1040 match tag {
1041 0 => rlx_ir::DType::F32,
1042 1 => rlx_ir::DType::F64,
1043 2 => rlx_ir::DType::C64,
1044 other => panic!("rlx-wgpu Op::Fft: bad dtype tag {other}"),
1045 }
1046}
1047
1048fn step_name(step: &Step) -> &'static str {
1049 match step {
1050 Step::CastF32ToF16 { .. } => "cast_f32_to_f16",
1051 Step::Matmul { .. } => "matmul",
1052 Step::Binary { .. } => "binary",
1053 Step::Compare { .. } => "compare",
1054 Step::Unary { .. } => "unary",
1055 Step::Where { .. } => "where",
1056 Step::Fma { .. } => "fma",
1057 Step::Reduce { .. } => "reduce",
1058 Step::Softmax { .. } => "softmax",
1059 Step::SoftmaxCrossEntropy { .. } => "softmax_cross_entropy",
1060 Step::LayerNorm { .. } => "layer_norm",
1061 Step::Cumsum { .. } => "cumsum",
1062 Step::FftGpu { .. } => "fft_gpu",
1063 Step::FftHost { .. } => "fft_host",
1064 Step::WelchPeaksHost { .. } => "welch_peaks_host",
1065 Step::LogMelHost { .. } => "log_mel_host",
1066 Step::LogMelBackwardHost { .. } => "log_mel_backward_host",
1067 Step::Im2ColHost { .. } => "im2col_host",
1068 Step::RngNormalHost { .. } => "rng_normal_host",
1069 Step::RngUniformHost { .. } => "rng_uniform_host",
1070 Step::BufferCopy { .. } => "buffer_copy",
1071 Step::Copy { .. } => "copy",
1072 Step::Transpose { .. } => "transpose",
1073 Step::Narrow { .. } => "narrow",
1074 Step::Concat { .. } => "concat",
1075 Step::Gather { .. } => "gather",
1076 Step::GatherAxis { .. } => "gather_axis",
1077 Step::Attention { .. } => "attention",
1078 Step::AttentionBackward { .. } => "attention_bwd",
1079 Step::Rope { .. } => "rope",
1080 Step::Expand { .. } => "expand",
1081 Step::Argmax { .. } => "argmax",
1082 Step::Pool2d { .. } => "pool2d",
1083 Step::Conv2d { .. } => "conv2d",
1084 Step::Pool1d { .. } => "pool1d",
1085 Step::Pool3d { .. } => "pool3d",
1086 Step::Conv1d { .. } => "conv1d",
1087 Step::Conv3d { .. } => "conv3d",
1088 Step::ScatterAdd { .. } => "scatter_add",
1089 Step::TopK { .. } => "topk",
1090 Step::WelchPeaksGpu { .. } => "welch_peaks_gpu",
1091 Step::GroupedMatmul { .. } => "grouped_matmul",
1092 Step::Sample { .. } => "sample",
1093 Step::SelectiveScan { .. } => "selective_scan",
1094 Step::Mamba2 { .. } => "mamba2",
1095 Step::Gru { .. } => "gru",
1096 Step::Rnn { .. } => "rnn",
1097 Step::GruHost { .. } => "gru_host",
1098 Step::RnnHost { .. } => "rnn_host",
1099 Step::DequantMatmul { .. } => "dequant_matmul",
1100 Step::GatherSplit { .. } => "gather_split",
1101 Step::DequantMatmulGguf { .. } => "dequant_matmul_gguf",
1102 Step::DequantGroupedMatmulGguf { .. } => "dequant_grouped_matmul_gguf",
1103 Step::GatedDeltaNet { .. } => "gated_delta_net",
1104 Step::Lstm { .. } => "lstm",
1105 Step::ConvTranspose2d { .. } => "conv_transpose2d",
1106 Step::GroupNormHost { .. } => "group_norm_host",
1107 Step::LayerNorm2dHost { .. } => "layer_norm2d_host",
1108 Step::ResizeNearest2xHost { .. } => "resize_nearest2x_host",
1109 Step::ReverseHost { .. } => "reverse_host",
1110 Step::ArgReduceHost { .. } => "argreduce_host",
1111 Step::Llada2GroupLimitedGate { .. } => "llada2_group_limited_gate",
1112 Step::UmapKnn { .. } => "umap_knn",
1113 Step::UmapKnnHost { .. } => "umap_knn_host",
1114 Step::MsDeformAttnHost { .. } => "ms_deform_attn_host",
1115 #[cfg(feature = "splat")]
1116 Step::GaussianSplatRender { .. } => "gaussian_splat_render",
1117 #[cfg(feature = "splat")]
1118 Step::GaussianSplatRenderBackward { .. } => "gaussian_splat_render_backward",
1119 #[cfg(feature = "splat")]
1120 Step::GaussianSplatPrepare { .. } => "gaussian_splat_prepare",
1121 #[cfg(feature = "splat")]
1122 Step::GaussianSplatRasterize { .. } => "gaussian_splat_rasterize",
1123 Step::RmsNormBackwardInput { .. } => "rms_norm_backward_input",
1124 Step::RmsNormBackwardGamma { .. } => "rms_norm_backward_gamma",
1125 Step::RmsNormBackwardBeta { .. } => "rms_norm_backward_beta",
1126 Step::LayerNormBackwardInput { .. } => "layer_norm_backward_input",
1127 Step::LayerNormBackwardGammaPartial { .. } => "layer_norm_backward_gamma_partial",
1128 Step::LayerNormBackwardGammaReduce { .. } => "layer_norm_backward_gamma_reduce",
1129 Step::RopeBackward { .. } => "rope_backward",
1130 Step::CumsumBackward { .. } => "cumsum_backward",
1131 Step::GatherBackward { .. } => "gather_backward",
1132 Step::FusedResidualLn { .. } => "fused_residual_ln",
1133 Step::FusedResidualLnTee { .. } => "fused_residual_ln_tee",
1134 Step::FusedResidualRmsNorm { .. } => "fused_residual_rms_norm",
1135 Step::MatmulQkv { .. } => "matmul_qkv",
1136 Step::ElementwiseRegion { .. } => "elementwise_region",
1137 Step::BatchElementwiseRegion { .. } => "batch_elementwise_region",
1138 }
1139}
1140
1141fn step_is_tail_host(step: &Step) -> bool {
1142 matches!(
1143 step,
1144 Step::WelchPeaksHost { .. } | Step::LogMelHost { .. } | Step::LogMelBackwardHost { .. }
1145 )
1146}
1147
1148fn step_runs_on_host(step: &Step) -> bool {
1149 match step {
1150 Step::GatherSplit { .. }
1151 | Step::DequantMatmulGguf { .. }
1152 | Step::DequantGroupedMatmulGguf { .. }
1153 | Step::GatedDeltaNet { .. }
1154 | Step::Lstm { .. }
1155 | Step::ConvTranspose2d { .. }
1156 | Step::GroupNormHost { .. }
1157 | Step::LayerNorm2dHost { .. }
1158 | Step::ResizeNearest2xHost { .. }
1159 | Step::ReverseHost { .. }
1160 | Step::ArgReduceHost { .. }
1161 | Step::GruHost { .. }
1162 | Step::RnnHost { .. }
1163 | Step::Llada2GroupLimitedGate { .. }
1164 | Step::UmapKnnHost { .. }
1165 | Step::MsDeformAttnHost { .. }
1166 | Step::FftHost { .. }
1167 | Step::Im2ColHost { .. }
1168 | Step::RngNormalHost { .. }
1169 | Step::RngUniformHost { .. }
1170 | Step::BufferCopy { .. } => true,
1171 #[cfg(feature = "splat")]
1172 Step::GaussianSplatRender { .. }
1173 | Step::GaussianSplatRenderBackward { .. }
1174 | Step::GaussianSplatPrepare { .. }
1175 | Step::GaussianSplatRasterize { .. } => true,
1176 _ => false,
1177 }
1178}
1179
1180fn binary_op_id(op: BinaryOp) -> u32 {
1181 match op {
1182 BinaryOp::Add => 0,
1183 BinaryOp::Sub => 1,
1184 BinaryOp::Mul => 2,
1185 BinaryOp::Div => 3,
1186 BinaryOp::Max => 4,
1187 BinaryOp::Min => 5,
1188 BinaryOp::Pow => 6,
1189 }
1190}
1191
1192fn compare_op_id(op: CmpOp) -> u32 {
1193 match op {
1194 CmpOp::Eq => 0,
1195 CmpOp::Ne => 1,
1196 CmpOp::Lt => 2,
1197 CmpOp::Le => 3,
1198 CmpOp::Gt => 4,
1199 CmpOp::Ge => 5,
1200 }
1201}
1202
1203fn reduce_op_id(op: ReduceOp) -> u32 {
1204 match op {
1205 ReduceOp::Sum => 0,
1206 ReduceOp::Mean => 1,
1207 ReduceOp::Max => 2,
1208 ReduceOp::Min => 3,
1209 ReduceOp::Prod => 4,
1210 }
1211}
1212
1213fn activation_op_id(act: Activation) -> u32 {
1214 match act {
1215 Activation::Relu => 0,
1216 Activation::Sigmoid => 1,
1217 Activation::Tanh => 2,
1218 Activation::Exp => 3,
1219 Activation::Log => 4,
1220 Activation::Sqrt => 5,
1221 Activation::Rsqrt => 6,
1222 Activation::Neg => 7,
1223 Activation::Abs => 8,
1224 Activation::Gelu => 9,
1225 Activation::Silu => 10,
1226 Activation::GeluApprox => 11,
1227 Activation::Round => 12,
1228 Activation::Sin => 13,
1229 Activation::Cos => 14,
1230 Activation::Tan => 15,
1231 Activation::Atan => 16,
1232 }
1233}
1234
1235impl WgpuExecutable {
1236 fn lazy_compile_for_inputs(&mut self, inputs: &[(&str, &[f32])]) {
1240 let unresolved = self
1241 .unresolved
1242 .as_ref()
1243 .expect("lazy_compile_for_inputs called without an unresolved graph");
1244 let binding = infer_bindings_from_f32_inputs(unresolved, inputs)
1245 .expect("rlx-wgpu lazy compile: could not infer DimBinding from inputs");
1246
1247 if let Some(prev) = &self.last_binding
1249 && same_binding(prev, &binding)
1250 {
1251 return;
1252 }
1253
1254 let resolved = bind_graph(unresolved, &binding);
1256 let original = self.unresolved.take();
1257 let pending_params = std::mem::take(&mut self.pending_params);
1258 let pending_bytes = std::mem::take(&mut self.pending_param_bytes);
1259
1260 let fresh = Self::compile_static_inner(resolved, self.rng.clone());
1261
1262 self.graph = fresh.graph;
1265 self.arena = fresh.arena;
1266 self.dequant_scratch_off = fresh.dequant_scratch_off;
1267 self.schedule = fresh.schedule;
1268 self.input_offsets = fresh.input_offsets;
1269 self.param_offsets = fresh.param_offsets;
1270 self.uniforms = fresh.uniforms;
1271 self.bind_groups = fresh.bind_groups;
1272 self.meta_buffers = fresh.meta_buffers;
1273 self.unresolved = original;
1274 self.last_binding = Some(binding);
1275 self.uniforms_active_extent = None;
1278 self.input_staging_hashes.clear();
1279 self.coop_f16_vk = fresh.coop_f16_vk;
1280 self.coop_f16_b_param = fresh.coop_f16_b_param;
1281 self.coop_f16_vk_wide_bind_groups = fresh.coop_f16_vk_wide_bind_groups;
1282 self.coop_f16_host_activations = fresh.coop_f16_host_activations;
1283
1284 for (name, data) in pending_params {
1286 self.set_param(&name, &data);
1287 }
1288 for (name, data) in pending_bytes {
1289 self.set_param_bytes(&name, &data);
1290 }
1291 }
1292
1293 pub fn compile_with_bindings(graph: Graph, bindings: &DimBinding) -> Self {
1299 if bindings.is_empty() {
1300 return Self::compile(graph);
1301 }
1302 let mut fresh = Graph::new(&graph.name);
1304 for node in graph.nodes() {
1305 let bound = node.shape.bind(bindings);
1306 fresh.add_node(node.op.clone(), node.inputs.clone(), bound);
1307 }
1308 fresh.set_outputs(graph.outputs.clone());
1309 Self::compile(fresh)
1310 }
1311
1312 pub fn compile(graph: Graph) -> Self {
1313 Self::compile_rng(graph, rlx_ir::RngOptions::default())
1314 }
1315
1316 pub fn compile_rng(graph: Graph, rng: rlx_ir::RngOptions) -> Self {
1317 let rng = std::sync::Arc::new(std::sync::RwLock::new(rng));
1318 if has_dynamic_dims(&graph) {
1319 return Self::deferred(graph, rng);
1320 }
1321 Self::compile_static_inner(graph, rng)
1322 }
1323
1324 pub fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
1326 *self.rng.write().expect("rng lock") = rng;
1327 }
1328
1329 pub fn rng(&self) -> rlx_ir::RngOptions {
1331 *self.rng.read().expect("rng lock")
1332 }
1333
1334 #[doc(hidden)]
1336 pub fn test_attn_q_seq_stride(&self) -> Option<u32> {
1337 self.schedule.iter().find_map(|s| {
1338 if let Step::Attention { params, .. } = s {
1339 Some(params.q_seq_stride)
1340 } else {
1341 None
1342 }
1343 })
1344 }
1345
1346 #[doc(hidden)]
1348 pub fn test_attn_offsets_and_stride(&self) -> Option<(u32, u32, u32, u32)> {
1349 self.schedule.iter().find_map(|s| {
1350 if let Step::Attention { params, .. } = s {
1351 Some((
1352 params.q_off,
1353 params.k_off,
1354 params.v_off,
1355 params.q_seq_stride,
1356 ))
1357 } else {
1358 None
1359 }
1360 })
1361 }
1362
1363 #[doc(hidden)]
1365 pub fn test_arena_offset_elems(&self, id: NodeId) -> u32 {
1366 (self.arena.offset(id) / 4) as u32
1367 }
1368
1369 fn deferred(graph: Graph, rng: std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>) -> Self {
1374 let dev = wgpu_device().expect("rlx-wgpu: no compatible adapter found");
1375 let placeholder = dev.device.create_buffer(&wgpu::BufferDescriptor {
1377 label: Some("rlx-wgpu deferred placeholder"),
1378 size: 16,
1379 usage: wgpu::BufferUsages::STORAGE
1380 | wgpu::BufferUsages::COPY_DST
1381 | wgpu::BufferUsages::COPY_SRC,
1382 mapped_at_creation: false,
1383 });
1384 let arena = Arena {
1385 buffer: placeholder,
1386 f16_buffer: None,
1387 offsets: HashMap::new(),
1388 lens: HashMap::new(),
1389 size: 0,
1390 scratch_off: 0,
1391 scratch_bytes: 0,
1392 };
1393 Self {
1394 graph: graph.clone(),
1395 arena,
1396 dequant_scratch_off: 0,
1397 schedule: Vec::new(),
1398 input_offsets: HashMap::new(),
1399 param_offsets: HashMap::new(),
1400 uniforms: Vec::new(),
1401 bind_groups: Vec::new(),
1402 meta_buffers: Vec::new(),
1403 unresolved: Some(graph),
1404 last_binding: None,
1405 pending_params: HashMap::new(),
1406 pending_param_bytes: HashMap::new(),
1407 active_extent: None,
1408 uniforms_active_extent: None,
1409 input_staging_hashes: HashMap::new(),
1410 coop_f16_vk: false,
1411 coop_f16_b_param: HashMap::new(),
1412 coop_f16_vk_wide_b: HashSet::new(),
1413 coop_f16_vk_wide_bind_groups: HashMap::new(),
1414 coop_f16_host_activations: Vec::new(),
1415 stashed_params: HashMap::new(),
1416 readback_staging: None,
1417 tiny_readback: None,
1418 dispatch_only: false,
1419 fft_gpu_steps: Vec::new(),
1420 gpu_handles: HashMap::new(),
1421 gpu_handle_feeds: HashMap::new(),
1422 gpu_handle_resident: HashSet::new(),
1423 pending_read_indices: None,
1424 rng,
1425 }
1426 }
1427
1428 pub fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
1432 self.active_extent = extent;
1433 }
1434
1435 fn all_safe_for_active(&self) -> bool {
1436 self.schedule.iter().all(|s| s.safe_for_active_extent())
1437 }
1438
1439 fn compile_static_inner(
1440 graph: Graph,
1441 rng: std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
1442 ) -> Self {
1443 let dev = wgpu_device().expect("rlx-wgpu: no compatible adapter found");
1444
1445 let graph = crate::unfuse::unfuse(graph);
1452
1453 let mut plan = plan_f32_uniform(&graph, 16);
1455 let dequant_scratch = crate::gguf_gpu::dequant_gguf_scratch_bytes(&graph);
1456 let dequant_scratch_off = if dequant_scratch > 0 {
1457 let aligned = plan.arena_size.div_ceil(16) * 16;
1458 let new_size = aligned + dequant_scratch.div_ceil(16) * 16;
1459 if (new_size as u64) <= dev.device.limits().max_buffer_size {
1460 plan.arena_size = new_size;
1461 aligned
1462 } else {
1463 0
1464 }
1465 } else {
1466 0
1467 };
1468 let scratch_bytes = compute_scratch_bytes(&graph);
1472 let mut arena = Arena::from_plan_with_scratch(&dev.device, &plan, scratch_bytes);
1473 for node in graph.nodes() {
1477 let elems = node.shape.num_elements().unwrap_or(0);
1478 arena.set_actual_len(node.id, elems * 4);
1479 }
1480
1481 for node in graph.nodes() {
1487 if let Op::Constant { data } = &node.op
1488 && arena.has(node.id)
1489 && !data.is_empty()
1490 {
1491 let widened: Option<Vec<u8>> = match node.shape.dtype() {
1492 rlx_ir::DType::I64 => Some(
1493 data.chunks_exact(8)
1494 .flat_map(|c| {
1495 (i64::from_le_bytes(c.try_into().unwrap()) as f32).to_le_bytes()
1496 })
1497 .collect(),
1498 ),
1499 rlx_ir::DType::I32 => Some(
1500 data.chunks_exact(4)
1501 .flat_map(|c| {
1502 (i32::from_le_bytes(c.try_into().unwrap()) as f32).to_le_bytes()
1503 })
1504 .collect(),
1505 ),
1506 rlx_ir::DType::U32 => Some(
1507 data.chunks_exact(4)
1508 .flat_map(|c| {
1509 (u32::from_le_bytes(c.try_into().unwrap()) as f32).to_le_bytes()
1510 })
1511 .collect(),
1512 ),
1513 rlx_ir::DType::Bool | rlx_ir::DType::U8 => Some(
1514 data.iter()
1515 .flat_map(|&b| (b as f32).to_le_bytes())
1516 .collect(),
1517 ),
1518 rlx_ir::DType::I8 => Some(
1519 data.iter()
1520 .flat_map(|&b| ((b as i8) as f32).to_le_bytes())
1521 .collect(),
1522 ),
1523 _ => None,
1524 };
1525 let bytes: &[u8] = widened.as_deref().unwrap_or(data);
1526 let bytes_to_write = bytes.len().min(arena.len_of(node.id));
1527 dev.queue.write_buffer(
1528 &arena.buffer,
1529 arena.offset(node.id) as u64,
1530 &bytes[..bytes_to_write],
1531 );
1532 }
1533 }
1534
1535 let mut input_offsets = HashMap::new();
1536 let mut param_offsets = HashMap::new();
1537 for node in graph.nodes() {
1538 match &node.op {
1539 Op::Input { name } => {
1540 input_offsets.insert(name.clone(), node.id);
1541 }
1542 Op::Param { name } => {
1543 param_offsets.insert(name.clone(), node.id);
1544 }
1545 _ => {}
1546 }
1547 }
1548
1549 let mm_k = matmul_kernel(&dev.device);
1550 let mm_w = matmul_wide_kernel(&dev.device);
1551 let _mm_w_active = matmul_wide_active_kernel(&dev.device);
1552 let mm_f16w = matmul_f16w_kernel(&dev.device);
1553 let mm_f16c = matmul_f16_compute_kernel(&dev.device);
1554 let mm_coop = matmul_coop16_kernel(&dev.device);
1555 let mm_coop_f32 = matmul_coop_f32_active_kernel(&dev.device);
1556 let mm_cast = cast_f32_to_f16_kernel(&dev.device);
1557 let bk = binary_kernel(&dev.device);
1558 let uk = unary_kernel(&dev.device);
1559 let ck = compare_kernel(&dev.device);
1560 let wk = where_kernel(&dev.device);
1561 let fk = fma_kernel(&dev.device);
1562
1563 let mut schedule = Vec::new();
1564 let mut uniforms = Vec::new();
1565 let mut bind_groups = Vec::new();
1566 let mut fft_gpu_steps: Vec<crate::fft_dispatch::FftGpuResources> = Vec::new();
1567 let mut gguf_host_pad: Option<(wgpu::Buffer, wgpu::BindGroup)> = None;
1568 let mut meta_buffers: Vec<wgpu::Buffer> = Vec::new();
1569 let mut coop_f16_b_param: HashMap<u32, String> = HashMap::new();
1570 let mut coop_f16_vk_wide_bind_groups: HashMap<usize, wgpu::BindGroup> = HashMap::new();
1571 let mm_w_active_compile = matmul_wide_active_kernel(&dev.device);
1572
1573 let coop_f16_vk_mirror_acts = collect_coop_f16_vk_mirror_activations(&graph, &dev.device);
1574
1575 let mut qkv_split: HashMap<NodeId, (NodeId, NodeId, NodeId)> = HashMap::new();
1588 for (parent_id, qkv) in detect_split_qkv_pattern(&graph) {
1589 let parent = graph.node(parent_id);
1590 let a_id = parent.inputs[0];
1593 let b_id = parent.inputs[1];
1594 let a_dims = graph.node(a_id).shape.dims();
1595 let b_dims = graph.node(b_id).shape.dims();
1596 let out_dims = parent.shape.dims();
1597 let (m, k, n) =
1598 if a_dims.len() >= 2 && b_dims.len() == 2 && out_dims.len() == a_dims.len() {
1599 let leading: usize = a_dims[..a_dims.len() - 2]
1600 .iter()
1601 .map(|d| d.unwrap_static())
1602 .product();
1603 let m_inner = a_dims[a_dims.len() - 2].unwrap_static();
1604 let k_inner = a_dims[a_dims.len() - 1].unwrap_static();
1605 let n_inner = b_dims[1].unwrap_static();
1606 ((leading * m_inner) as u32, k_inner as u32, n_inner as u32)
1607 } else if a_dims.len() == 2 && b_dims.len() == 2 {
1608 (
1609 a_dims[0].unwrap_static() as u32,
1610 a_dims[1].unwrap_static() as u32,
1611 b_dims[1].unwrap_static() as u32,
1612 )
1613 } else {
1614 continue; };
1616 let cp = derive_matmul_compute(
1617 &dev.device,
1618 &graph,
1619 &coop_f16_vk_mirror_acts,
1620 a_id,
1621 b_id,
1622 m,
1623 k,
1624 n,
1625 );
1626 if cp == MatmulCompute::F32 || cp == MatmulCompute::CoopF32 {
1631 qkv_split.insert(parent_id, qkv);
1632 }
1633 }
1634 let qkv_skip_narrows: HashSet<NodeId> = qkv_split
1635 .values()
1636 .flat_map(|&(q, k, v)| [q, k, v])
1637 .collect();
1638
1639 let mut packed_bshd_attn: HashMap<NodeId, (NodeId, u32)> = HashMap::new();
1643 let mut packed_bshd_skip_narrows: HashSet<NodeId> = HashSet::new();
1644 if !rlx_ir::env::flag("RLX_WGPU_NO_PACKED_BSHD_ATTN") {
1645 for node in graph.nodes() {
1646 let Op::Attention { .. } = &node.op else {
1647 continue;
1648 };
1649 if node.inputs.len() < 3 {
1650 continue;
1651 }
1652 if let Some((parent, head_width, narrows)) =
1653 rlx_ir::detect_packed_bshd_qkv_attention(
1654 &graph,
1655 node.inputs[0],
1656 node.inputs[1],
1657 node.inputs[2],
1658 )
1659 {
1660 packed_bshd_attn.insert(node.id, (parent, head_width as u32));
1661 for narrow in narrows {
1662 if rlx_ir::packed_bshd_narrow_elidable(&graph, narrow, node.id) {
1663 packed_bshd_skip_narrows.insert(narrow);
1664 }
1665 }
1666 }
1667 }
1668 }
1669
1670 let (ln_to_tee, skip_adds) = detect_residual_ln_tee_pattern(&graph);
1680
1681 let mut coop_f16_host_activations: Vec<(NodeId, Activation, String)> = Vec::new();
1682
1683 let emit_uniform = |size: usize| -> wgpu::Buffer {
1684 dev.device.create_buffer(&wgpu::BufferDescriptor {
1685 label: Some("rlx-wgpu uniform"),
1686 size: size as u64,
1687 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1688 mapped_at_creation: false,
1689 })
1690 };
1691
1692 for node in graph.nodes() {
1693 let elems = node.shape.num_elements().unwrap_or(0) as u32;
1697 match &node.op {
1698 Op::Input { .. } | Op::Param { .. } | Op::Constant { .. } => continue,
1699 Op::MatMul => {
1700 let a_id = node.inputs[0];
1701 let b_id = node.inputs[1];
1702 let a_shape = graph.node(a_id).shape.dims();
1703 let b_shape = graph.node(b_id).shape.dims();
1704 let out_shape = node.shape.dims();
1705 let (m, k, n, batch, a_bs, b_bs, c_bs) = if a_shape.len() == 2
1710 && b_shape.len() == 2
1711 && out_shape.len() == 2
1712 {
1713 (
1714 a_shape[0].unwrap_static() as u32,
1715 a_shape[1].unwrap_static() as u32,
1716 b_shape[1].unwrap_static() as u32,
1717 1u32,
1718 0u32,
1719 0u32,
1720 0u32,
1721 )
1722 } else if a_shape.len() >= 2
1723 && b_shape.len() == 2
1724 && out_shape.len() == a_shape.len()
1725 {
1726 let leading: usize = a_shape[..a_shape.len() - 2]
1727 .iter()
1728 .map(|d| d.unwrap_static())
1729 .product();
1730 let m_inner = a_shape[a_shape.len() - 2].unwrap_static();
1731 let k_inner = a_shape[a_shape.len() - 1].unwrap_static();
1732 let n_inner = b_shape[1].unwrap_static();
1733 (
1734 (leading * m_inner) as u32,
1735 k_inner as u32,
1736 n_inner as u32,
1737 1u32,
1738 0u32,
1739 0u32,
1740 0u32,
1741 )
1742 } else if a_shape.len() == b_shape.len()
1743 && a_shape.len() >= 3
1744 && out_shape.len() == a_shape.len()
1745 {
1746 let leading_a: Vec<usize> = a_shape[..a_shape.len() - 2]
1748 .iter()
1749 .map(|d| d.unwrap_static())
1750 .collect();
1751 let leading_b: Vec<usize> = b_shape[..b_shape.len() - 2]
1752 .iter()
1753 .map(|d| d.unwrap_static())
1754 .collect();
1755 if leading_a != leading_b {
1756 panic!(
1757 "rlx-wgpu MatMul: batched shape mismatch \
1758 a_leading={leading_a:?} b_leading={leading_b:?}"
1759 );
1760 }
1761 let b_count: usize = leading_a.iter().product();
1762 let m_inner = a_shape[a_shape.len() - 2].unwrap_static();
1763 let k_inner = a_shape[a_shape.len() - 1].unwrap_static();
1764 let n_inner = b_shape[b_shape.len() - 1].unwrap_static();
1765 (
1766 m_inner as u32,
1767 k_inner as u32,
1768 n_inner as u32,
1769 b_count as u32,
1770 (m_inner * k_inner) as u32,
1771 (k_inner * n_inner) as u32,
1772 (m_inner * n_inner) as u32,
1773 )
1774 } else {
1775 panic!(
1776 "rlx-wgpu MatMul: unsupported shapes a={a_shape:?} b={b_shape:?} \
1777 out={out_shape:?} (supported: 2D×2D, [..,M,K]×[K,N], [..,M,K]×[..,K,N])"
1778 );
1779 };
1780 let b_is_param = tensor_is_graph_param(&graph, ¶m_offsets, b_id);
1781 let b_bytes = arena.len_of(b_id) as u64;
1782 let mut compute_precision = derive_matmul_compute(
1783 &dev.device,
1784 &graph,
1785 &coop_f16_vk_mirror_acts,
1786 a_id,
1787 b_id,
1788 m,
1789 k,
1790 n,
1791 );
1792 if b_is_param && b_bytes > ARENA_STAGE_CAP && arena.param_fits_f16_mirror(b_id)
1793 {
1794 compute_precision = MatmulCompute::F16;
1795 }
1796 let b_in_arena = !matmul_b_from_f16(compute_precision, b_is_param);
1797 let (mut base, mut size, param_anchor) = arena_matmul_bind_window(
1798 &dev.device,
1799 &arena,
1800 &graph,
1801 ¶m_offsets,
1802 node.id,
1803 a_id,
1804 b_id,
1805 b_in_arena,
1806 );
1807 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
1808 let expand_ids: &[NodeId] = if b_in_arena {
1810 &[node.id, a_id, b_id]
1811 } else {
1812 &[node.id, a_id]
1813 };
1814 arena_expand_bind_window(&arena, expand_ids, &mut base, &mut size, max_binding);
1815 let mut scratch = arena.scratch_off as u64;
1816 if param_anchor {
1817 arena_ensure_scratch_in_window(&mut scratch, base, size);
1818 }
1819 if b_is_param && b_bytes > ARENA_STAGE_CAP && b_in_arena {
1820 assert!(
1829 arena_tensor_in_window(&arena, b_id, base, size),
1830 "rlx-wgpu matmul: large param B {:?} off={} not in window base={base} size={size}",
1831 b_id,
1832 arena.offset(b_id),
1833 );
1834 }
1835 let a_off_f32 = arena_off_in_bind_window(
1836 &graph,
1837 ¶m_offsets,
1838 &dev.device,
1839 &arena,
1840 &mut schedule,
1841 &mut scratch,
1842 a_id,
1843 &mut base,
1844 &mut size,
1845 );
1846 let b_off_f32 = if !b_in_arena {
1847 (arena.offset(b_id) / 4) as u32
1852 } else if b_is_param
1853 && b_bytes > ARENA_STAGE_CAP
1854 && arena_tensor_in_window(&arena, b_id, base, size)
1855 {
1856 arena_local_off_f32(&arena, b_id, base)
1857 } else {
1858 arena_off_in_bind_window(
1859 &graph,
1860 ¶m_offsets,
1861 &dev.device,
1862 &arena,
1863 &mut schedule,
1864 &mut scratch,
1865 b_id,
1866 &mut base,
1867 &mut size,
1868 )
1869 };
1870 maybe_push_coop_f16_vk_casts(
1871 &graph,
1872 a_id,
1873 b_id,
1874 &coop_f16_vk_mirror_acts,
1875 &dev.device,
1876 &arena,
1877 &mut schedule,
1878 &mut uniforms,
1879 &mut bind_groups,
1880 &mm_cast,
1881 compute_precision,
1882 a_off_f32,
1883 m,
1884 k,
1885 batch,
1886 b_off_f32,
1887 n,
1888 );
1889 schedule.push(Step::Matmul {
1890 m,
1891 k,
1892 n,
1893 batch,
1894 a_batch_stride: a_bs,
1895 b_batch_stride: b_bs,
1896 c_batch_stride: c_bs,
1897 a_off_f32,
1898 b_off_f32,
1899 c_off_f32: arena_local_off_f32(&arena, node.id, base),
1900 has_bias: 0,
1901 bias_off_f32: 0,
1902 act_id: 0xFFFF,
1903 b_is_param,
1904 compute_precision,
1905 });
1906 let b_off_global = (arena.offset(b_id) / 4) as u32;
1907 let b_off_bind = if b_is_param
1908 && matches!(
1909 compute_precision,
1910 MatmulCompute::Coop16 | MatmulCompute::CoopF16Vk | MatmulCompute::F16
1911 ) {
1912 b_off_global
1913 } else {
1914 b_off_f32
1915 };
1916 register_coop_f16_vk_b_param(
1917 &mut coop_f16_b_param,
1918 ¶m_offsets,
1919 b_id,
1920 b_off_bind,
1921 compute_precision,
1922 );
1923 let u = emit_uniform(std::mem::size_of::<MatmulParams>());
1924 let (bg, b_off_adj) = build_matmul_bind_group(
1925 &dev.device,
1926 mm_k,
1927 mm_w,
1928 &mm_f16w,
1929 &mm_f16c,
1930 &mm_coop,
1931 &mm_coop_f32,
1932 &arena,
1933 base,
1934 size,
1935 &u,
1936 b_is_param,
1937 compute_precision,
1938 k,
1939 n,
1940 batch,
1941 b_off_bind,
1942 b_bs,
1943 );
1944 if let Some(Step::Matmul { b_off_f32, .. }) = schedule.last_mut() {
1945 *b_off_f32 = b_off_adj;
1946 }
1947 uniforms.push(u);
1948 bind_groups.push(bg);
1949 if compute_precision == MatmulCompute::CoopF16Vk {
1950 coop_f16_vk_wide_bind_groups.insert(
1951 schedule.len() - 1,
1952 bind_two_buf0_window(
1953 &dev.device,
1954 mm_w_active_compile,
1955 &arena.buffer,
1956 base,
1957 size,
1958 &uniforms[uniforms.len() - 1],
1959 ),
1960 );
1961 }
1962 }
1963 Op::Binary(bop) => {
1964 if skip_adds.contains(&node.id) {
1969 continue;
1970 }
1971 require_equal_shapes(&graph, &node.inputs, "Binary");
1972 let a_id = node.inputs[0];
1973 let b_id = node.inputs[1];
1974 let win_ids = [node.id, a_id, b_id];
1975 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
1976 let fits = arena_span_bytes(&arena, &win_ids) <= max_binding;
1977 let mut scratch = arena.scratch_off as u64;
1978 let (mut base, mut size, param_anchor) = arena_multi_op_window(
1979 &dev.device,
1980 &arena,
1981 &graph,
1982 ¶m_offsets,
1983 &mut schedule,
1984 &mut scratch,
1985 &win_ids,
1986 );
1987 if !fits && !param_anchor {
1988 base = arena_bind_window_covering_scratch_if_needed(
1989 &arena, base, size, scratch,
1990 );
1991 }
1992 let a_off = arena_off_in_bind_window(
1993 &graph,
1994 ¶m_offsets,
1995 &dev.device,
1996 &arena,
1997 &mut schedule,
1998 &mut scratch,
1999 a_id,
2000 &mut base,
2001 &mut size,
2002 );
2003 let b_off = arena_off_in_bind_window(
2004 &graph,
2005 ¶m_offsets,
2006 &dev.device,
2007 &arena,
2008 &mut schedule,
2009 &mut scratch,
2010 b_id,
2011 &mut base,
2012 &mut size,
2013 );
2014 let p = BinaryParams {
2015 n: elems,
2016 a_off,
2017 b_off,
2018 c_off: arena_local_off_f32(&arena, node.id, base),
2019 op: binary_op_id(*bop),
2020 _p0: 0,
2021 _p1: 0,
2022 _p2: 0,
2023 };
2024 schedule.push(Step::Binary { params: p });
2025 let u = emit_uniform(std::mem::size_of::<BinaryParams>());
2026 let bg = bind_two_buf0_window(&dev.device, bk, &arena.buffer, base, size, &u);
2027 uniforms.push(u);
2028 bind_groups.push(bg);
2029 }
2030 Op::Compare(cop) => {
2031 require_equal_shapes(&graph, &node.inputs, "Compare");
2032 let (mut base, size) = arena_window_for_nodes(&dev.device, &arena, &[node.id]);
2033 let a_id = node.inputs[0];
2034 let b_id = node.inputs[1];
2035 let a_src = arena.offset(a_id) as u64;
2036 let b_src = arena.offset(b_id) as u64;
2037 let a_len = arena.len_of(a_id) as u64;
2038 let b_len = arena.len_of(b_id) as u64;
2039 let a_in = a_src >= base && a_src + a_len <= base + size;
2040 let b_in = b_src >= base && b_src + b_len <= base + size;
2041 let a_dst = arena.scratch_off as u64;
2042 let a_aligned = a_len.div_ceil(256) * 256;
2043 let b_dst = a_dst + a_aligned;
2044 if a_dst < base || b_dst + b_len > base + size {
2045 base = (arena.size as u64).saturating_sub(size);
2046 base = (base / 256) * 256;
2047 }
2048 let a_off = if a_in {
2049 arena_local_off_f32(&arena, a_id, base)
2050 } else {
2051 if a_len > 64 * 1024 * 1024 {
2052 panic!("rlx-wgpu: Compare staging operand A too large ({a_len} bytes)");
2053 }
2054 schedule.push(Step::BufferCopy {
2055 src_byte_off: a_src,
2056 dst_byte_off: a_dst,
2057 bytes: a_len as u32,
2058 });
2059 ((a_dst.saturating_sub(base)) / 4) as u32
2060 };
2061 let b_off = if b_in {
2062 arena_local_off_f32(&arena, b_id, base)
2063 } else {
2064 if b_len > 64 * 1024 * 1024 {
2065 panic!("rlx-wgpu: Compare staging operand B too large ({b_len} bytes)");
2066 }
2067 schedule.push(Step::BufferCopy {
2068 src_byte_off: b_src,
2069 dst_byte_off: b_dst,
2070 bytes: b_len as u32,
2071 });
2072 ((b_dst.saturating_sub(base)) / 4) as u32
2073 };
2074 let p = BinaryParams {
2075 n: elems,
2076 a_off,
2077 b_off,
2078 c_off: arena_local_off_f32(&arena, node.id, base),
2079 op: compare_op_id(*cop),
2080 _p0: 0,
2081 _p1: 0,
2082 _p2: 0,
2083 };
2084 schedule.push(Step::Compare { params: p });
2085 let u = emit_uniform(std::mem::size_of::<BinaryParams>());
2086 let bg = bind_two_buf0_window(&dev.device, ck, &arena.buffer, base, size, &u);
2087 uniforms.push(u);
2088 bind_groups.push(bg);
2089 }
2090 Op::Activation(act) => {
2091 if coop_f16_vk_mirror_acts.contains(&node.id) {
2092 let src_name =
2093 tensor_host_name(&input_offsets, ¶m_offsets, node.inputs[0]);
2094 coop_f16_host_activations.push((node.id, *act, src_name));
2095 continue;
2096 }
2097 let in_id = node.inputs[0];
2098 let win_ids = [node.id, in_id];
2099 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
2100 let fits = arena_span_bytes(&arena, &win_ids) <= max_binding;
2101 let mut scratch = arena.scratch_off as u64;
2102 let (mut base, mut size, param_anchor) = arena_multi_op_window(
2103 &dev.device,
2104 &arena,
2105 &graph,
2106 ¶m_offsets,
2107 &mut schedule,
2108 &mut scratch,
2109 &win_ids,
2110 );
2111 if !fits && !param_anchor {
2112 base = arena_bind_window_covering_scratch_if_needed(
2113 &arena, base, size, scratch,
2114 );
2115 }
2116 let in_off = arena_off_in_bind_window(
2117 &graph,
2118 ¶m_offsets,
2119 &dev.device,
2120 &arena,
2121 &mut schedule,
2122 &mut scratch,
2123 in_id,
2124 &mut base,
2125 &mut size,
2126 );
2127 let p = UnaryParams {
2128 n: elems,
2129 in_off,
2130 out_off: arena_local_off_f32(&arena, node.id, base),
2131 op: activation_op_id(*act),
2132 _p0: 0,
2133 _p1: 0,
2134 _p2: 0,
2135 _p3: 0,
2136 };
2137 schedule.push(Step::Unary {
2138 params: p,
2139 f16_mirror: false,
2140 });
2141 let u = emit_uniform(std::mem::size_of::<UnaryParams>());
2142 let bg = bind_two_buf0_window(&dev.device, uk, &arena.buffer, base, size, &u);
2143 uniforms.push(u);
2144 bind_groups.push(bg);
2145 }
2146 Op::Where => {
2147 let (mut base, size) = arena_window_for_nodes(&dev.device, &arena, &[node.id]);
2148 let cond_id = node.inputs[0];
2149 let x_id = node.inputs[1];
2150 let y_id = node.inputs[2];
2151 let cond_src = arena.offset(cond_id) as u64;
2152 let x_src = arena.offset(x_id) as u64;
2153 let y_src = arena.offset(y_id) as u64;
2154 let cond_len = arena.len_of(cond_id) as u64;
2155 let x_len = arena.len_of(x_id) as u64;
2156 let y_len = arena.len_of(y_id) as u64;
2157 let cond_in = cond_src >= base && cond_src + cond_len <= base + size;
2158 let x_in = x_src >= base && x_src + x_len <= base + size;
2159 let y_in = y_src >= base && y_src + y_len <= base + size;
2160 let cond_dst = arena.scratch_off as u64;
2161 let cond_aligned = cond_len.div_ceil(256) * 256;
2162 let x_dst = cond_dst + cond_aligned;
2163 let x_aligned = x_len.div_ceil(256) * 256;
2164 let y_dst = x_dst + x_aligned;
2165 if cond_dst < base || y_dst + y_len > base + size {
2166 base = (arena.size as u64).saturating_sub(size);
2167 base = (base / 256) * 256;
2168 }
2169 let cond_off = if cond_in {
2170 arena_local_off_f32(&arena, cond_id, base)
2171 } else {
2172 if cond_len > 64 * 1024 * 1024 {
2173 panic!("rlx-wgpu: Where staging cond too large ({cond_len} bytes)");
2174 }
2175 schedule.push(Step::BufferCopy {
2176 src_byte_off: cond_src,
2177 dst_byte_off: cond_dst,
2178 bytes: cond_len as u32,
2179 });
2180 ((cond_dst.saturating_sub(base)) / 4) as u32
2181 };
2182 let x_off = if x_in {
2183 arena_local_off_f32(&arena, x_id, base)
2184 } else {
2185 if x_len > 64 * 1024 * 1024 {
2186 panic!("rlx-wgpu: Where staging x too large ({x_len} bytes)");
2187 }
2188 schedule.push(Step::BufferCopy {
2189 src_byte_off: x_src,
2190 dst_byte_off: x_dst,
2191 bytes: x_len as u32,
2192 });
2193 ((x_dst.saturating_sub(base)) / 4) as u32
2194 };
2195 let y_off = if y_in {
2196 arena_local_off_f32(&arena, y_id, base)
2197 } else {
2198 if y_len > 64 * 1024 * 1024 {
2199 panic!("rlx-wgpu: Where staging y too large ({y_len} bytes)");
2200 }
2201 schedule.push(Step::BufferCopy {
2202 src_byte_off: y_src,
2203 dst_byte_off: y_dst,
2204 bytes: y_len as u32,
2205 });
2206 ((y_dst.saturating_sub(base)) / 4) as u32
2207 };
2208 let p = WhereParams {
2209 n: elems,
2210 cond_off,
2211 x_off,
2212 y_off,
2213 out_off: arena_local_off_f32(&arena, node.id, base),
2214 _p0: 0,
2215 _p1: 0,
2216 _p2: 0,
2217 };
2218 schedule.push(Step::Where { params: p });
2219 let u = emit_uniform(std::mem::size_of::<WhereParams>());
2220 let bg = bind_two_buf0_window(&dev.device, wk, &arena.buffer, base, size, &u);
2221 uniforms.push(u);
2222 bind_groups.push(bg);
2223 }
2224
2225 Op::Fma => {
2226 let (mut base, size) = arena_window_for_nodes(&dev.device, &arena, &[node.id]);
2227 let a_id = node.inputs[0];
2228 let b_id = node.inputs[1];
2229 let c_id = node.inputs[2];
2230 let a_src = arena.offset(a_id) as u64;
2231 let b_src = arena.offset(b_id) as u64;
2232 let c_src = arena.offset(c_id) as u64;
2233 let a_len = arena.len_of(a_id) as u64;
2234 let b_len = arena.len_of(b_id) as u64;
2235 let c_len = arena.len_of(c_id) as u64;
2236 let a_in = a_src >= base && a_src + a_len <= base + size;
2237 let b_in = b_src >= base && b_src + b_len <= base + size;
2238 let c_in = c_src >= base && c_src + c_len <= base + size;
2239 let a_dst = arena.scratch_off as u64;
2240 let a_aligned = a_len.div_ceil(256) * 256;
2241 let b_dst = a_dst + a_aligned;
2242 let b_aligned = b_len.div_ceil(256) * 256;
2243 let c_dst = b_dst + b_aligned;
2244 if a_dst < base || c_dst + c_len > base + size {
2245 base = (arena.size as u64).saturating_sub(size);
2246 base = (base / 256) * 256;
2247 }
2248 let a_off = if a_in {
2249 arena_local_off_f32(&arena, a_id, base)
2250 } else {
2251 if a_len > 64 * 1024 * 1024 {
2252 panic!("rlx-wgpu: Fma staging a too large ({a_len} bytes)");
2253 }
2254 schedule.push(Step::BufferCopy {
2255 src_byte_off: a_src,
2256 dst_byte_off: a_dst,
2257 bytes: a_len as u32,
2258 });
2259 ((a_dst.saturating_sub(base)) / 4) as u32
2260 };
2261 let b_off = if b_in {
2262 arena_local_off_f32(&arena, b_id, base)
2263 } else {
2264 if b_len > 64 * 1024 * 1024 {
2265 panic!("rlx-wgpu: Fma staging b too large ({b_len} bytes)");
2266 }
2267 schedule.push(Step::BufferCopy {
2268 src_byte_off: b_src,
2269 dst_byte_off: b_dst,
2270 bytes: b_len as u32,
2271 });
2272 ((b_dst.saturating_sub(base)) / 4) as u32
2273 };
2274 let c_off = if c_in {
2275 arena_local_off_f32(&arena, c_id, base)
2276 } else {
2277 if c_len > 64 * 1024 * 1024 {
2278 panic!("rlx-wgpu: Fma staging c too large ({c_len} bytes)");
2279 }
2280 schedule.push(Step::BufferCopy {
2281 src_byte_off: c_src,
2282 dst_byte_off: c_dst,
2283 bytes: c_len as u32,
2284 });
2285 ((c_dst.saturating_sub(base)) / 4) as u32
2286 };
2287 let p = FmaParams {
2288 n: elems,
2289 a_off,
2290 b_off,
2291 c_off,
2292 out_off: arena_local_off_f32(&arena, node.id, base),
2293 _p0: 0,
2294 _p1: 0,
2295 _p2: 0,
2296 };
2297 schedule.push(Step::Fma { params: p });
2298 let u = emit_uniform(std::mem::size_of::<FmaParams>());
2299 let bg = bind_two_buf0_window(&dev.device, fk, &arena.buffer, base, size, &u);
2300 uniforms.push(u);
2301 bind_groups.push(bg);
2302 }
2303
2304 Op::BatchElementwiseRegion {
2305 chain,
2306 num_batch_inputs,
2307 scalar_input_mask,
2308 input_modulus,
2309 prologue,
2310 prologue_input,
2311 } => {
2312 let n = *num_batch_inputs as usize;
2313 if n == 0 || chain.len() > 32 {
2314 panic!(
2315 "rlx-wgpu BatchElementwiseRegion: num_batch_inputs={n} steps={}",
2316 chain.len()
2317 );
2318 }
2319 let slice_shape = rlx_ir::batch_region_slice_shape(&node.shape);
2320 let slice_elems = rlx_ir::batch_region_slice_elems(&node.shape, n)
2321 .expect("batch region static shape");
2322 let mut win_ids: Vec<NodeId> = vec![node.id];
2323 win_ids.extend(node.inputs.iter().copied());
2324 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
2325 let fits = arena_span_bytes(&arena, &win_ids) <= max_binding;
2326 let mut scratch = arena.scratch_off as u64;
2327 let (mut base, mut size, param_anchor) = arena_multi_op_window(
2328 &dev.device,
2329 &arena,
2330 &graph,
2331 ¶m_offsets,
2332 &mut schedule,
2333 &mut scratch,
2334 &win_ids,
2335 );
2336 if !fits && !param_anchor {
2337 base = arena_bind_window_covering_scratch_if_needed(
2338 &arena, base, size, scratch,
2339 );
2340 }
2341 let chain_enc = rlx_ir::encode_chain_steps(chain);
2342 let tail =
2343 rlx_ir::encode_prologue_tail(*prologue, &slice_shape, *prologue_input);
2344 let base_dst = arena_local_off_f32(&arena, node.id, base);
2345 let use_single = rlx_ir::fk_batch_use_single_launch(n, *prologue);
2346 if use_single {
2347 let mut batch_input_offs = [0u32; 64];
2348 for i in 0..n {
2349 batch_input_offs[i] = arena_off_in_bind_window(
2350 &graph,
2351 ¶m_offsets,
2352 &dev.device,
2353 &arena,
2354 &mut schedule,
2355 &mut scratch,
2356 node.inputs[i],
2357 &mut base,
2358 &mut size,
2359 );
2360 }
2361 let p = BatchElementwiseRegionParams {
2362 slice_len: slice_elems,
2363 num_batch: n as u32,
2364 num_steps: chain.len() as u32,
2365 base_dst_off: base_dst,
2366 slice_elems,
2367 batch_input_offs,
2368 chain: chain_enc,
2369 scalar_input_mask: *scalar_input_mask,
2370 input_modulus: *input_modulus,
2371 };
2372 schedule.push(Step::BatchElementwiseRegion { params: p });
2373 let ek = batch_elementwise_region_kernel(&dev.device);
2374 let u = dev.device.create_buffer(&wgpu::BufferDescriptor {
2375 label: Some("rlx-wgpu batch region params"),
2376 size: std::mem::size_of::<BatchElementwiseRegionParams>() as u64,
2377 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2378 mapped_at_creation: false,
2379 });
2380 let bg =
2381 bind_two_buf0_window(&dev.device, ek, &arena.buffer, base, size, &u);
2382 uniforms.push(u);
2383 bind_groups.push(bg);
2384 } else {
2385 let spatial = tail[0] == rlx_ir::REGION_PROLOGUE_RESIZE_NEAREST_2X_NCHW;
2386 let ek = if spatial {
2387 elementwise_region_spatial_kernel(&dev.device)
2388 } else {
2389 elementwise_region_kernel(&dev.device)
2390 };
2391 for i in 0..n {
2392 let mut input_offs = [0u32; 16];
2393 input_offs[0] = arena_off_in_bind_window(
2394 &graph,
2395 ¶m_offsets,
2396 &dev.device,
2397 &arena,
2398 &mut schedule,
2399 &mut scratch,
2400 node.inputs[i],
2401 &mut base,
2402 &mut size,
2403 );
2404 let p = ElementwiseRegionParams {
2405 len: slice_elems,
2406 num_inputs: 1,
2407 num_steps: chain.len() as u32,
2408 dst_off: rlx_ir::batch_region_slice_dst_off_f32(
2409 base_dst,
2410 slice_elems,
2411 i,
2412 ),
2413 input_offs,
2414 chain: chain_enc,
2415 scalar_input_mask: *scalar_input_mask,
2416 prologue: tail[0],
2417 out_n: tail[1],
2418 out_c: tail[2],
2419 out_h: tail[3],
2420 out_w: tail[4],
2421 prologue_input: tail[5],
2422 input_modulus: *input_modulus,
2423 };
2424 schedule.push(Step::ElementwiseRegion { params: p });
2425 let u = dev.device.create_buffer(&wgpu::BufferDescriptor {
2426 label: Some("rlx-wgpu batch region params"),
2427 size: std::mem::size_of::<ElementwiseRegionParams>() as u64,
2428 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2429 mapped_at_creation: false,
2430 });
2431 let bg = bind_two_buf0_window(
2432 &dev.device,
2433 ek,
2434 &arena.buffer,
2435 base,
2436 size,
2437 &u,
2438 );
2439 uniforms.push(u);
2440 bind_groups.push(bg);
2441 }
2442 }
2443 }
2444 Op::ElementwiseRegion {
2445 chain,
2446 num_inputs,
2447 scalar_input_mask,
2448 input_modulus,
2449 prologue,
2450 prologue_input,
2451 } => {
2452 let n = *num_inputs as usize;
2455 if n > 16 || chain.len() > 32 {
2456 panic!(
2457 "rlx-wgpu ElementwiseRegion: chain too large \
2458 (inputs={n}, steps={}). Caps: 16 / 32. \
2459 Use UnfuseElementwiseRegions to fall back.",
2460 chain.len()
2461 );
2462 }
2463 let mut win_ids: Vec<NodeId> = vec![node.id];
2464 win_ids.extend(node.inputs.iter().copied());
2465 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
2466 let fits = arena_span_bytes(&arena, &win_ids) <= max_binding;
2467 let mut scratch = arena.scratch_off as u64;
2468 let (mut base, mut size, param_anchor) = arena_multi_op_window(
2469 &dev.device,
2470 &arena,
2471 &graph,
2472 ¶m_offsets,
2473 &mut schedule,
2474 &mut scratch,
2475 &win_ids,
2476 );
2477 if !fits && !param_anchor {
2478 base = arena_bind_window_covering_scratch_if_needed(
2479 &arena, base, size, scratch,
2480 );
2481 }
2482 let mut input_offs = [0u32; 16];
2483 for (i, &id) in node.inputs.iter().enumerate() {
2484 input_offs[i] = arena_off_in_bind_window(
2485 &graph,
2486 ¶m_offsets,
2487 &dev.device,
2488 &arena,
2489 &mut schedule,
2490 &mut scratch,
2491 id,
2492 &mut base,
2493 &mut size,
2494 );
2495 }
2496 let chain_enc = rlx_ir::encode_chain_steps(chain);
2497 let tail =
2498 rlx_ir::encode_prologue_tail(*prologue, &node.shape, *prologue_input);
2499 let p = ElementwiseRegionParams {
2500 len: elems,
2501 num_inputs: *num_inputs,
2502 num_steps: chain.len() as u32,
2503 dst_off: arena_local_off_f32(&arena, node.id, base),
2504 input_offs,
2505 chain: chain_enc,
2506 scalar_input_mask: *scalar_input_mask,
2507 prologue: tail[0],
2508 out_n: tail[1],
2509 out_c: tail[2],
2510 out_h: tail[3],
2511 out_w: tail[4],
2512 prologue_input: tail[5],
2513 input_modulus: *input_modulus,
2514 };
2515 schedule.push(Step::ElementwiseRegion { params: p });
2516 let ek = if p.prologue == rlx_ir::REGION_PROLOGUE_RESIZE_NEAREST_2X_NCHW {
2517 elementwise_region_spatial_kernel(&dev.device)
2518 } else {
2519 elementwise_region_kernel(&dev.device)
2520 };
2521 let u = dev.device.create_buffer(&wgpu::BufferDescriptor {
2525 label: Some("rlx-wgpu region params"),
2526 size: std::mem::size_of::<ElementwiseRegionParams>() as u64,
2527 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2528 mapped_at_creation: false,
2529 });
2530 let bg = bind_two_buf0_window(&dev.device, ek, &arena.buffer, base, size, &u);
2531 uniforms.push(u);
2532 bind_groups.push(bg);
2533 }
2534
2535 Op::Reduce {
2536 op: rop,
2537 axes,
2538 keep_dim: _,
2539 } => {
2540 let in_id = node.inputs[0];
2548 let in_shape = graph.node(in_id).shape.dims();
2549 let mut sorted = axes.clone();
2550 sorted.sort_unstable();
2551 let contiguous = sorted.windows(2).all(|w| w[1] == w[0] + 1);
2552 if !contiguous {
2553 panic!(
2554 "rlx-wgpu Reduce: non-contiguous axes not yet wired \
2555 (got axes={axes:?}, rank={})",
2556 in_shape.len()
2557 );
2558 }
2559 let ax_first = sorted[0];
2560 let ax_last = *sorted.last().unwrap();
2561 let dims_u32: Vec<u32> =
2562 in_shape.iter().map(|d| d.unwrap_static() as u32).collect();
2563 let outer: u32 = dims_u32[..ax_first].iter().product();
2564 let reduce_dim: u32 = dims_u32[ax_first..=ax_last].iter().product();
2565 let inner: u32 = dims_u32[ax_last + 1..].iter().product();
2566 let red_ids = [node.id, in_id];
2567 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
2568 let red_fits = arena_span_bytes(&arena, &red_ids) <= max_binding;
2569 let mut scratch = arena.scratch_off as u64;
2570 let (mut base, mut size, param_anchor) = arena_multi_op_window(
2571 &dev.device,
2572 &arena,
2573 &graph,
2574 ¶m_offsets,
2575 &mut schedule,
2576 &mut scratch,
2577 &red_ids,
2578 );
2579 if !red_fits && !param_anchor {
2580 base = arena_bind_window_covering_scratch_if_needed(
2581 &arena, base, size, scratch,
2582 );
2583 }
2584 let in_off = arena_off_in_bind_window(
2585 &graph,
2586 ¶m_offsets,
2587 &dev.device,
2588 &arena,
2589 &mut schedule,
2590 &mut scratch,
2591 in_id,
2592 &mut base,
2593 &mut size,
2594 );
2595 let p = ReduceParams {
2596 outer,
2597 reduce_dim,
2598 inner,
2599 in_off,
2600 out_off: arena_local_off_f32(&arena, node.id, base),
2601 op: reduce_op_id(*rop),
2602 _p0: 0,
2603 _p1: 0,
2604 };
2605 schedule.push(Step::Reduce { params: p });
2606 let rk = reduce_kernel(&dev.device);
2607 let u = emit_uniform(std::mem::size_of::<ReduceParams>());
2608 let bg = bind_two_buf0_window(&dev.device, rk, &arena.buffer, base, size, &u);
2609 uniforms.push(u);
2610 bind_groups.push(bg);
2611 }
2612
2613 Op::Softmax { axis } => {
2614 let in_id = node.inputs[0];
2615 let in_shape = graph.node(in_id).shape.dims();
2616 let last = (in_shape.len() - 1) as i32;
2617 if *axis != -1 && *axis != last {
2618 panic!("rlx-wgpu Softmax: only last-axis wired (got axis={axis})");
2619 }
2620 let inner = in_shape[in_shape.len() - 1].unwrap_static() as u32;
2621 let total: u32 = in_shape.iter().map(|d| d.unwrap_static() as u32).product();
2622 let outer = total / inner.max(1);
2623 let sm_ids = [node.id, in_id];
2624 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
2625 let sm_fits = arena_span_bytes(&arena, &sm_ids) <= max_binding;
2626 let mut scratch = arena.scratch_off as u64;
2627 let (mut base, mut size, param_anchor) = arena_multi_op_window(
2628 &dev.device,
2629 &arena,
2630 &graph,
2631 ¶m_offsets,
2632 &mut schedule,
2633 &mut scratch,
2634 &sm_ids,
2635 );
2636 if !sm_fits && !param_anchor {
2637 base = arena_bind_window_covering_scratch_if_needed(
2638 &arena, base, size, scratch,
2639 );
2640 }
2641 let in_off = arena_off_in_bind_window(
2642 &graph,
2643 ¶m_offsets,
2644 &dev.device,
2645 &arena,
2646 &mut schedule,
2647 &mut scratch,
2648 in_id,
2649 &mut base,
2650 &mut size,
2651 );
2652 let p = SoftmaxParams {
2653 outer,
2654 inner,
2655 in_off,
2656 out_off: arena_local_off_f32(&arena, node.id, base),
2657 _p0: 0,
2658 _p1: 0,
2659 _p2: 0,
2660 _p3: 0,
2661 };
2662 schedule.push(Step::Softmax { params: p });
2663 let sk = softmax_kernel(&dev.device);
2664 let u = emit_uniform(std::mem::size_of::<SoftmaxParams>());
2665 let bg = bind_two_buf0_window(&dev.device, sk, &arena.buffer, base, size, &u);
2666 uniforms.push(u);
2667 bind_groups.push(bg);
2668 }
2669
2670 Op::SoftmaxCrossEntropy => {
2671 let logits_id = node.inputs[0];
2676 let targets_id = node.inputs[1];
2677 let logits_shape = graph.node(logits_id).shape.dims();
2678 let inner = logits_shape[logits_shape.len() - 1].unwrap_static() as u32;
2679 let total: u32 = logits_shape
2680 .iter()
2681 .map(|d| d.unwrap_static() as u32)
2682 .product();
2683 let outer = total / inner.max(1);
2684
2685 let sce_win = vec![node.id, logits_id, targets_id];
2686 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
2687 let sce_fits = arena_span_bytes(&arena, &sce_win) <= max_binding;
2688 let mut scratch = arena.scratch_off as u64;
2689 let (mut base, mut size, param_anchor) = arena_multi_op_window(
2690 &dev.device,
2691 &arena,
2692 &graph,
2693 ¶m_offsets,
2694 &mut schedule,
2695 &mut scratch,
2696 &sce_win,
2697 );
2698 if !sce_fits && !param_anchor {
2699 base = arena_bind_window_covering_scratch_if_needed(
2700 &arena, base, size, scratch,
2701 );
2702 }
2703 let logits_off = arena_off_in_bind_window(
2704 &graph,
2705 ¶m_offsets,
2706 &dev.device,
2707 &arena,
2708 &mut schedule,
2709 &mut scratch,
2710 logits_id,
2711 &mut base,
2712 &mut size,
2713 );
2714 let targets_off = arena_off_in_bind_window(
2715 &graph,
2716 ¶m_offsets,
2717 &dev.device,
2718 &arena,
2719 &mut schedule,
2720 &mut scratch,
2721 targets_id,
2722 &mut base,
2723 &mut size,
2724 );
2725 let p = SceParams {
2726 outer,
2727 inner,
2728 logits_off,
2729 targets_off,
2730 out_off: arena_local_off_f32(&arena, node.id, base),
2731 _p0: 0,
2732 _p1: 0,
2733 _p2: 0,
2734 };
2735 schedule.push(Step::SoftmaxCrossEntropy { params: p });
2736 let sk = softmax_cross_entropy_kernel(&dev.device);
2737 let u = emit_uniform(std::mem::size_of::<SceParams>());
2738 let bg = bind_two_buf0_window(&dev.device, sk, &arena.buffer, base, size, &u);
2739 uniforms.push(u);
2740 bind_groups.push(bg);
2741 }
2742
2743 Op::LayerNorm { axis: _, eps } | Op::RmsNorm { axis: _, eps } => {
2744 let in_id = node.inputs[0];
2745 let in_shape = graph.node(in_id).shape.dims();
2746 let inner = in_shape[in_shape.len() - 1].unwrap_static() as u32;
2747 let total: u32 = in_shape.iter().map(|d| d.unwrap_static() as u32).product();
2748 let outer = total / inner.max(1);
2749 let is_layer_norm = matches!(&node.op, Op::LayerNorm { .. });
2750
2751 if is_layer_norm
2758 && let Some(&(h_id, delta_id, gamma_id, beta_id, sum_id)) =
2759 ln_to_tee.get(&node.id)
2760 {
2761 let gamma_is_param =
2762 tensor_is_graph_param(&graph, ¶m_offsets, gamma_id);
2763 let gamma_bytes = arena.len_of(gamma_id) as u64;
2764 let frlt_win: Vec<NodeId> =
2765 if gamma_is_param && gamma_bytes > ARENA_STAGE_CAP {
2766 vec![gamma_id, node.id, h_id, delta_id, beta_id, sum_id]
2767 } else {
2768 vec![node.id, h_id, delta_id, gamma_id, beta_id, sum_id]
2769 };
2770 let mut scratch = arena.scratch_off as u64;
2771 let (mut base, mut size, param_anchor) = arena_multi_op_window(
2772 &dev.device,
2773 &arena,
2774 &graph,
2775 ¶m_offsets,
2776 &mut schedule,
2777 &mut scratch,
2778 &frlt_win,
2779 );
2780 if !param_anchor {
2781 base = arena_bind_window_covering_scratch_if_needed(
2782 &arena, base, size, scratch,
2783 );
2784 }
2785 let in_off = arena_off_in_bind_window(
2786 &graph,
2787 ¶m_offsets,
2788 &dev.device,
2789 &arena,
2790 &mut schedule,
2791 &mut scratch,
2792 h_id,
2793 &mut base,
2794 &mut size,
2795 );
2796 let residual_off = arena_off_in_bind_window(
2797 &graph,
2798 ¶m_offsets,
2799 &dev.device,
2800 &arena,
2801 &mut schedule,
2802 &mut scratch,
2803 delta_id,
2804 &mut base,
2805 &mut size,
2806 );
2807 let sum_off = arena_off_in_bind_window(
2808 &graph,
2809 ¶m_offsets,
2810 &dev.device,
2811 &arena,
2812 &mut schedule,
2813 &mut scratch,
2814 sum_id,
2815 &mut base,
2816 &mut size,
2817 );
2818 let gamma_off = arena_off_in_bind_window(
2819 &graph,
2820 ¶m_offsets,
2821 &dev.device,
2822 &arena,
2823 &mut schedule,
2824 &mut scratch,
2825 gamma_id,
2826 &mut base,
2827 &mut size,
2828 );
2829 let beta_off = arena_off_in_bind_window(
2830 &graph,
2831 ¶m_offsets,
2832 &dev.device,
2833 &arena,
2834 &mut schedule,
2835 &mut scratch,
2836 beta_id,
2837 &mut base,
2838 &mut size,
2839 );
2840 let p = FusedResidualLnTeeParams {
2841 outer,
2842 inner,
2843 in_off,
2844 residual_off,
2845 bias_off: 0, gamma_off,
2847 beta_off,
2848 sum_off,
2849 ln_out_off: arena_local_off_f32(&arena, node.id, base),
2850 eps_bits: eps.to_bits(),
2851 has_bias: 0,
2852 _p0: 0,
2853 };
2854 schedule.push(Step::FusedResidualLnTee { params: p });
2855 let frtk = fused_residual_ln_tee_kernel(&dev.device);
2856 let u = emit_uniform(std::mem::size_of::<FusedResidualLnTeeParams>());
2857 let bg =
2858 bind_two_buf0_window(&dev.device, frtk, &arena.buffer, base, size, &u);
2859 uniforms.push(u);
2860 bind_groups.push(bg);
2861 continue;
2862 }
2863
2864 let gamma_id = node.inputs[1];
2865 let beta_id = if is_layer_norm && node.inputs.len() >= 3 {
2868 node.inputs[2]
2869 } else {
2870 gamma_id
2873 };
2874 let gamma_is_param = tensor_is_graph_param(&graph, ¶m_offsets, gamma_id);
2875 let gamma_bytes = arena.len_of(gamma_id) as u64;
2876 let ln_win: Vec<NodeId> = if gamma_is_param && gamma_bytes > ARENA_STAGE_CAP {
2877 vec![gamma_id, node.id, in_id]
2878 } else {
2879 let mut v = vec![node.id, in_id];
2880 if gamma_is_param {
2881 v.push(gamma_id);
2882 }
2883 if is_layer_norm {
2884 v.push(beta_id);
2885 }
2886 v
2887 };
2888 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
2889 let ln_fits = arena_span_bytes(&arena, &ln_win) <= max_binding;
2890 let mut scratch = arena.scratch_off as u64;
2891 let (mut base, mut size, param_anchor) = arena_multi_op_window(
2892 &dev.device,
2893 &arena,
2894 &graph,
2895 ¶m_offsets,
2896 &mut schedule,
2897 &mut scratch,
2898 &ln_win,
2899 );
2900 if !ln_fits && !param_anchor {
2901 base = arena_bind_window_covering_scratch_if_needed(
2902 &arena, base, size, scratch,
2903 );
2904 }
2905 let in_off = arena_off_in_bind_window(
2906 &graph,
2907 ¶m_offsets,
2908 &dev.device,
2909 &arena,
2910 &mut schedule,
2911 &mut scratch,
2912 in_id,
2913 &mut base,
2914 &mut size,
2915 );
2916 let gamma_off = arena_off_in_bind_window(
2917 &graph,
2918 ¶m_offsets,
2919 &dev.device,
2920 &arena,
2921 &mut schedule,
2922 &mut scratch,
2923 gamma_id,
2924 &mut base,
2925 &mut size,
2926 );
2927 let beta_off = arena_off_in_bind_window(
2928 &graph,
2929 ¶m_offsets,
2930 &dev.device,
2931 &arena,
2932 &mut schedule,
2933 &mut scratch,
2934 beta_id,
2935 &mut base,
2936 &mut size,
2937 );
2938 let p = LayerNormParams {
2939 outer,
2940 inner,
2941 in_off,
2942 out_off: arena_local_off_f32(&arena, node.id, base),
2943 gamma_off,
2944 beta_off,
2945 eps_bits: eps.to_bits(),
2946 op: if is_layer_norm { 0 } else { 1 },
2947 };
2948 schedule.push(Step::LayerNorm { params: p });
2949 let lk = layernorm_kernel(&dev.device);
2950 let u = emit_uniform(std::mem::size_of::<LayerNormParams>());
2951 let bg = bind_two_buf0_window(&dev.device, lk, &arena.buffer, base, size, &u);
2952 uniforms.push(u);
2953 bind_groups.push(bg);
2954 }
2955
2956 Op::Reshape { .. } => {
2957 }
2959
2960 Op::Cast { .. } => {
2961 let in_id = node.inputs[0];
2968 let src = arena.offset(in_id);
2969 let dst = arena.offset(node.id);
2970 if src != dst {
2971 let bytes = arena.len_of(in_id).min(arena.len_of(node.id));
2972 schedule.push(Step::BufferCopy {
2973 src_byte_off: src as u64,
2974 dst_byte_off: dst as u64,
2975 bytes: bytes as u32,
2976 });
2977 }
2978 }
2979
2980 Op::Transpose { perm } => {
2981 let in_id = node.inputs[0];
2982 let in_shape = graph.node(in_id).shape.dims();
2983 let out_shape = node.shape.dims();
2984 let rank = perm.len();
2985 if rank != in_shape.len() || rank != out_shape.len() {
2986 panic!("rlx-wgpu Transpose: rank mismatch");
2987 }
2988 let in_dims: Vec<u32> =
2989 in_shape.iter().map(|d| d.unwrap_static() as u32).collect();
2990 let out_dims: Vec<u32> =
2991 out_shape.iter().map(|d| d.unwrap_static() as u32).collect();
2992 let mut in_strides = vec![1u32; rank];
2994 for i in (0..rank.saturating_sub(1)).rev() {
2995 in_strides[i] = in_strides[i + 1] * in_dims[i + 1];
2996 }
2997 let strides_for_out: Vec<u32> =
3000 (0..rank).map(|i| in_strides[perm[i]]).collect();
3001
3002 let mut meta_data: Vec<u32> = Vec::with_capacity(rank * 2);
3004 meta_data.extend_from_slice(&out_dims);
3005 meta_data.extend_from_slice(&strides_for_out);
3006 let meta_buf = dev.device.create_buffer(&wgpu::BufferDescriptor {
3007 label: Some("rlx-wgpu transpose meta"),
3008 size: (meta_data.len() * 4).max(4) as u64,
3009 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
3010 mapped_at_creation: false,
3011 });
3012 dev.queue
3013 .write_buffer(&meta_buf, 0, bytemuck::cast_slice(&meta_data));
3014 let meta_idx = meta_buffers.len();
3015 meta_buffers.push(meta_buf);
3016
3017 let bucket_outermost = if perm[0] == 0 { 1u32 } else { 0u32 };
3021 let tr_ids = [node.id, in_id];
3022 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
3023 let in_is_param = tensor_is_graph_param(&graph, ¶m_offsets, in_id);
3024 let in_bytes = arena.len_of(in_id) as u64;
3025 let (mut base, mut size) = if in_is_param && in_bytes <= max_binding {
3026 arena_window_for_nodes(&dev.device, &arena, &[in_id])
3027 } else if arena_span_bytes(&arena, &tr_ids) <= max_binding {
3028 arena_window_for_nodes(&dev.device, &arena, &tr_ids)
3029 } else {
3030 arena_window_for_nodes(&dev.device, &arena, &[node.id])
3031 };
3032 let mut scratch = arena.scratch_off as u64;
3033 let in_off = arena_off_in_bind_window(
3034 &graph,
3035 ¶m_offsets,
3036 &dev.device,
3037 &arena,
3038 &mut schedule,
3039 &mut scratch,
3040 in_id,
3041 &mut base,
3042 &mut size,
3043 );
3044 let out_off = arena_off_in_bind_window(
3045 &graph,
3046 ¶m_offsets,
3047 &dev.device,
3048 &arena,
3049 &mut schedule,
3050 &mut scratch,
3051 node.id,
3052 &mut base,
3053 &mut size,
3054 );
3055 let p = TransposeParams {
3056 rank: rank as u32,
3057 out_total: elems,
3058 in_off,
3059 out_off,
3060 bucket_outermost,
3061 out_dim_0: out_dims[0],
3062 _p2: 0,
3063 _p3: 0,
3064 };
3065 schedule.push(Step::Transpose {
3066 params: p,
3067 meta_idx,
3068 });
3069 let tk = transpose_kernel(&dev.device);
3070 let u = emit_uniform(std::mem::size_of::<TransposeParams>());
3071 let bg = dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
3072 label: Some("rlx-wgpu transpose bg"),
3073 layout: &tk.bgl,
3074 entries: &[
3075 wgpu::BindGroupEntry {
3076 binding: 0,
3077 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3078 buffer: &arena.buffer,
3079 offset: base,
3080 size: NonZeroU64::new(size),
3081 }),
3082 },
3083 wgpu::BindGroupEntry {
3084 binding: 1,
3085 resource: u.as_entire_binding(),
3086 },
3087 wgpu::BindGroupEntry {
3088 binding: 2,
3089 resource: meta_buffers[meta_idx].as_entire_binding(),
3090 },
3091 ],
3092 });
3093 uniforms.push(u);
3094 bind_groups.push(bg);
3095 }
3096
3097 Op::Narrow { axis, start, len } => {
3098 if qkv_skip_narrows.contains(&node.id)
3103 || packed_bshd_skip_narrows.contains(&node.id)
3104 {
3105 continue;
3106 }
3107 let in_id = node.inputs[0];
3108 let in_shape = graph.node(in_id).shape.dims();
3109 let outer: u32 = in_shape[..*axis]
3110 .iter()
3111 .map(|d| d.unwrap_static() as u32)
3112 .product::<u32>()
3113 .max(1);
3114 let inner: u32 = in_shape[*axis + 1..]
3115 .iter()
3116 .map(|d| d.unwrap_static() as u32)
3117 .product::<u32>()
3118 .max(1);
3119 let axis_in = in_shape[*axis].unwrap_static() as u32;
3120 let win_ids = [node.id, in_id];
3121 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
3122 let fits = arena_span_bytes(&arena, &win_ids) <= max_binding;
3123 let mut scratch = arena.scratch_off as u64;
3124 let (mut base, mut size, param_anchor) = arena_multi_op_window(
3125 &dev.device,
3126 &arena,
3127 &graph,
3128 ¶m_offsets,
3129 &mut schedule,
3130 &mut scratch,
3131 &win_ids,
3132 );
3133 if !fits && !param_anchor {
3134 base = arena_bind_window_covering_scratch_if_needed(
3135 &arena, base, size, scratch,
3136 );
3137 }
3138 let in_off = arena_off_in_bind_window(
3139 &graph,
3140 ¶m_offsets,
3141 &dev.device,
3142 &arena,
3143 &mut schedule,
3144 &mut scratch,
3145 in_id,
3146 &mut base,
3147 &mut size,
3148 );
3149 let out_off = arena_off_in_bind_window(
3150 &graph,
3151 ¶m_offsets,
3152 &dev.device,
3153 &arena,
3154 &mut schedule,
3155 &mut scratch,
3156 node.id,
3157 &mut base,
3158 &mut size,
3159 );
3160 let p = NarrowConcatParams {
3161 total: elems,
3162 outer,
3163 inner,
3164 axis_in_size: axis_in,
3165 axis_out_size: *len as u32,
3166 start: *start as u32,
3167 in_off,
3168 out_off,
3169 };
3170 schedule.push(Step::Narrow { params: p });
3171 let nk = narrow_kernel(&dev.device);
3172 let u = emit_uniform(std::mem::size_of::<NarrowConcatParams>());
3173 let bg = bind_two_buf0_window(&dev.device, nk, &arena.buffer, base, size, &u);
3174 uniforms.push(u);
3175 bind_groups.push(bg);
3176 }
3177
3178 Op::Concat { axis } => {
3179 let out_shape = node.shape.dims();
3180 let outer: u32 = out_shape[..*axis]
3181 .iter()
3182 .map(|d| d.unwrap_static() as u32)
3183 .product::<u32>()
3184 .max(1);
3185 let inner: u32 = out_shape[*axis + 1..]
3186 .iter()
3187 .map(|d| d.unwrap_static() as u32)
3188 .product::<u32>()
3189 .max(1);
3190 let axis_out = out_shape[*axis].unwrap_static() as u32;
3191
3192 let all_ids: Vec<NodeId> = std::iter::once(node.id)
3193 .chain(node.inputs.iter().copied())
3194 .collect();
3195 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
3196 let fits_all = arena_span_bytes(&arena, &all_ids) <= max_binding;
3197 let mut scratch = arena.scratch_off as u64;
3198 let (mut base, mut size, param_anchor) = arena_multi_op_window(
3199 &dev.device,
3200 &arena,
3201 &graph,
3202 ¶m_offsets,
3203 &mut schedule,
3204 &mut scratch,
3205 &all_ids,
3206 );
3207 arena_expand_bind_window(&arena, &all_ids, &mut base, &mut size, max_binding);
3208 if !fits_all && !param_anchor {
3209 base = arena_bind_window_covering_scratch_if_needed(
3210 &arena, base, size, scratch,
3211 );
3212 }
3213 let mut start_pos: u32 = 0;
3214 for &in_id in &node.inputs {
3215 let in_shape = graph.node(in_id).shape.dims();
3216 let axis_in = in_shape[*axis].unwrap_static() as u32;
3217 let in_total: u32 =
3218 in_shape.iter().map(|d| d.unwrap_static() as u32).product();
3219 let _win_ids = [node.id, in_id];
3220 let in_off = arena_off_in_bind_window(
3221 &graph,
3222 ¶m_offsets,
3223 &dev.device,
3224 &arena,
3225 &mut schedule,
3226 &mut scratch,
3227 in_id,
3228 &mut base,
3229 &mut size,
3230 );
3231 let out_off = arena_local_off_f32(&arena, node.id, base);
3237 let p = NarrowConcatParams {
3238 total: in_total,
3239 outer,
3240 inner,
3241 axis_in_size: axis_in,
3242 axis_out_size: axis_out,
3243 start: start_pos,
3244 in_off,
3245 out_off,
3246 };
3247 schedule.push(Step::Concat { params: p });
3248 let cck = concat_kernel(&dev.device);
3249 let u = emit_uniform(std::mem::size_of::<NarrowConcatParams>());
3250 let bg =
3251 bind_two_buf0_window(&dev.device, cck, &arena.buffer, base, size, &u);
3252 uniforms.push(u);
3253 bind_groups.push(bg);
3254 start_pos += axis_in;
3255 }
3256 }
3257
3258 Op::Attention {
3259 num_heads,
3260 head_dim,
3261 mask_kind,
3262 score_scale: _,
3263 attn_logit_softcap: _,
3264 } => {
3265 let q_id = node.inputs[0];
3268 let k_id = node.inputs[1];
3269 let v_id = node.inputs[2];
3270 let q_shape = graph.node(q_id).shape.dims();
3271 let k_shape = graph.node(k_id).shape.dims();
3272 let h = *num_heads as u32;
3278 let hd = *head_dim as u32;
3279 let q_ir = graph.node(q_id).shape.clone();
3280 let k_ir = graph.node(k_id).shape.clone();
3281 let geom = rlx_ir::attention_geom(&q_ir, &k_ir, *num_heads, *head_dim);
3282 let bhsd = geom.bhsd;
3283 let (batch, heads, seq_q, seq_k) = match q_shape.len() {
3284 4 => (
3285 geom.batch as u32,
3286 geom.heads as u32,
3287 geom.seq_q as u32,
3288 geom.seq_k as u32,
3289 ),
3290 3 => {
3291 let last = q_shape[2].unwrap_static() as u32;
3298 if last == h * hd {
3299 (
3301 q_shape[0].unwrap_static() as u32,
3302 h,
3303 q_shape[1].unwrap_static() as u32,
3304 k_shape[1].unwrap_static() as u32,
3305 )
3306 } else {
3307 let leading = q_shape[0].unwrap_static() as u32;
3309 if !leading.is_multiple_of(h) {
3310 panic!(
3311 "rlx-wgpu Attention: rank-3 leading dim {leading} \
3312 not divisible by num_heads {h} (and last dim \
3313 {last} ≠ H·D = {})",
3314 h * hd
3315 );
3316 }
3317 (
3318 leading / h,
3319 h,
3320 q_shape[1].unwrap_static() as u32,
3321 k_shape[1].unwrap_static() as u32,
3322 )
3323 }
3324 }
3325 other => panic!(
3326 "rlx-wgpu Attention: only rank-3 / rank-4 Q,K,V \
3327 inputs supported (got rank {other})"
3328 ),
3329 };
3330 let scale = 1.0_f32 / (hd as f32).sqrt();
3331
3332 let (mask_kind_id, mask_buf, window) = match mask_kind {
3333 MaskKind::None => (0u32, None, 0u32),
3334 MaskKind::Causal => (1u32, None, 0u32),
3335 MaskKind::Custom => (2u32, None, 0u32),
3341 MaskKind::Bias => (4u32, None, 0u32),
3342 MaskKind::SlidingWindow(w) => (3u32, None, *w as u32),
3343 };
3344
3345 struct MStrides {
3352 b: u32,
3353 h: u32,
3354 q: u32,
3355 k: u32,
3356 }
3357 let mask_strides = if mask_kind_id == 2u32 || mask_kind_id == 4u32 {
3358 let m_dims = graph.node(node.inputs[3]).shape.dims();
3359 let dim = |i: usize| m_dims[i].unwrap_static() as u32;
3360 match m_dims.len() {
3361 2 => MStrides {
3362 b: dim(1),
3363 h: 0,
3364 q: 0,
3365 k: 1,
3366 },
3367 3 => MStrides {
3368 b: dim(1) * dim(2),
3369 h: 0,
3370 q: dim(2),
3371 k: 1,
3372 },
3373 4 => MStrides {
3374 b: dim(1) * dim(2) * dim(3),
3375 h: dim(2) * dim(3),
3376 q: dim(3),
3377 k: 1,
3378 },
3379 _ => MStrides {
3380 b: heads * seq_q * seq_k,
3381 h: seq_q * seq_k,
3382 q: seq_k,
3383 k: 1,
3384 },
3385 }
3386 } else {
3387 MStrides {
3388 b: heads * seq_q * seq_k,
3389 h: seq_q * seq_k,
3390 q: seq_k,
3391 k: 1,
3392 }
3393 };
3394
3395 let stride = |shape: &[rlx_ir::shape::Dim], seq_extent: u32| {
3396 rlx_ir::strides_for_shape(shape, heads, hd, seq_extent, bhsd)
3397 };
3398 let packed_parent = packed_bshd_attn.get(&node.id).copied();
3399 let (q_b, q_h, q_s, k_b, k_h, k_s, v_b, v_h, v_s) =
3400 if let Some((_parent, head_width)) = packed_parent {
3401 let (batch_stride, head_stride, pack_seq) =
3402 rlx_ir::packed_bshd_qkv_strides(head_width as usize, hd, seq_q);
3403 (
3404 batch_stride,
3405 head_stride,
3406 pack_seq,
3407 batch_stride,
3408 head_stride,
3409 pack_seq,
3410 batch_stride,
3411 head_stride,
3412 pack_seq,
3413 )
3414 } else {
3415 let (qb, qh, qs) = stride(q_shape, seq_q);
3416 let (kb, kh, ks) = stride(k_shape, seq_k);
3417 let v_shape = graph.node(v_id).shape.dims();
3418 let (vb, vh, vs) = stride(v_shape, seq_k);
3419 (qb, qh, qs, kb, kh, ks, vb, vh, vs)
3420 };
3421 let out_shape = node.shape.dims();
3422 let (o_b, o_h, o_s) = stride(out_shape, seq_q);
3423 let mut attn_ids = if let Some((parent, _)) = packed_parent {
3424 vec![node.id, parent]
3425 } else {
3426 vec![node.id, q_id, k_id, v_id]
3427 };
3428 if mask_kind_id == 2 {
3429 attn_ids.push(node.inputs[3]);
3430 }
3431 let mut scratch = arena.scratch_off as u64;
3432 let (mut base, mut size, param_anchor) = arena_multi_op_window(
3433 &dev.device,
3434 &arena,
3435 &graph,
3436 ¶m_offsets,
3437 &mut schedule,
3438 &mut scratch,
3439 &attn_ids,
3440 );
3441 if !param_anchor {
3442 base = arena_bind_window_covering_scratch_if_needed(
3443 &arena, base, size, scratch,
3444 );
3445 }
3446 let (q_off, k_off, v_off) = if let Some((parent, head_width)) = packed_parent {
3447 let parent_off = arena_off_in_bind_window(
3448 &graph,
3449 ¶m_offsets,
3450 &dev.device,
3451 &arena,
3452 &mut schedule,
3453 &mut scratch,
3454 parent,
3455 &mut base,
3456 &mut size,
3457 );
3458 (
3459 parent_off,
3460 parent_off.saturating_add(head_width),
3461 parent_off.saturating_add(head_width * 2),
3462 )
3463 } else {
3464 let q_off = arena_off_in_bind_window(
3465 &graph,
3466 ¶m_offsets,
3467 &dev.device,
3468 &arena,
3469 &mut schedule,
3470 &mut scratch,
3471 q_id,
3472 &mut base,
3473 &mut size,
3474 );
3475 let k_off = arena_off_in_bind_window(
3476 &graph,
3477 ¶m_offsets,
3478 &dev.device,
3479 &arena,
3480 &mut schedule,
3481 &mut scratch,
3482 k_id,
3483 &mut base,
3484 &mut size,
3485 );
3486 let v_off = arena_off_in_bind_window(
3487 &graph,
3488 ¶m_offsets,
3489 &dev.device,
3490 &arena,
3491 &mut schedule,
3492 &mut scratch,
3493 v_id,
3494 &mut base,
3495 &mut size,
3496 );
3497 (q_off, k_off, v_off)
3498 };
3499 let out_byte = arena.offset(node.id) as u64;
3500 let out_len = arena.len_of(node.id) as u64;
3501 let out_aliases_qkv = arena_tensors_overlap(&arena, node.id, q_id)
3502 || arena_tensors_overlap(&arena, node.id, k_id)
3503 || arena_tensors_overlap(&arena, node.id, v_id)
3504 || packed_parent.is_some_and(|(parent, _)| {
3505 arena_tensors_overlap(&arena, node.id, parent)
3506 });
3507 let mut kernel_out_off = arena_off_in_bind_window(
3508 &graph,
3509 ¶m_offsets,
3510 &dev.device,
3511 &arena,
3512 &mut schedule,
3513 &mut scratch,
3514 node.id,
3515 &mut base,
3516 &mut size,
3517 );
3518 let mut attn_scratch_copy: Option<(u64, u32)> = None;
3519 if out_aliases_qkv && rlx_ir::env::flag("RLX_WGPU_DEBUG_ATTN_ALIAS") {
3520 eprintln!(
3521 "rlx-wgpu Attention alias: out={:?}@{}+{} q={:?}@{} k={:?}@{} v={:?}@{}",
3522 node.id,
3523 out_byte,
3524 out_len,
3525 q_id,
3526 arena.offset(q_id),
3527 k_id,
3528 arena.offset(k_id),
3529 v_id,
3530 arena.offset(v_id),
3531 );
3532 }
3533 if out_aliases_qkv {
3534 let tmp_byte = scratch;
3535 let tmp_aligned = out_len.div_ceil(256) * 256;
3536 scratch = scratch.saturating_add(tmp_aligned);
3537 if param_anchor {
3538 arena_ensure_scratch_in_window(&mut scratch, base, size);
3539 } else {
3540 base = arena_bind_window_covering_scratch_if_needed(
3541 &arena, base, size, scratch,
3542 );
3543 }
3544 kernel_out_off = ((tmp_byte.saturating_sub(base)) / 4) as u32;
3545 attn_scratch_copy = Some((tmp_byte, out_len as u32));
3546 }
3547 let mask_off = if mask_kind_id == 2 {
3548 arena_off_in_bind_window(
3549 &graph,
3550 ¶m_offsets,
3551 &dev.device,
3552 &arena,
3553 &mut schedule,
3554 &mut scratch,
3555 node.inputs[3],
3556 &mut base,
3557 &mut size,
3558 )
3559 } else {
3560 0
3561 };
3562 let p = AttentionParams {
3563 batch,
3564 heads,
3565 seq_q,
3566 seq_k,
3567 head_dim: hd,
3568 q_off,
3569 k_off,
3570 v_off,
3571 out_off: kernel_out_off,
3572 mask_off,
3573 mask_kind: mask_kind_id,
3574 scale_bits: scale.to_bits(),
3575 window,
3576 seq_q_stride: mask_strides.q,
3585 seq_k_stride: mask_strides.k,
3586 mask_batch_stride: mask_strides.b,
3587 mask_head_stride: mask_strides.h,
3588 _pad_mask_0: 0,
3589 _pad_mask_1: 0,
3590 _pad_mask_2: 0,
3591 q_batch_stride: q_b,
3592 q_head_stride: q_h,
3593 q_seq_stride: q_s,
3594 _pad_q: 0,
3595 k_batch_stride: k_b,
3596 k_head_stride: k_h,
3597 k_seq_stride: k_s,
3598 _pad_k: 0,
3599 v_batch_stride: v_b,
3600 v_head_stride: v_h,
3601 v_seq_stride: v_s,
3602 _pad_v: 0,
3603 o_batch_stride: o_b,
3604 o_head_stride: o_h,
3605 o_seq_stride: o_s,
3606 _pad_o: 0,
3607 };
3608 let _ = num_heads;
3609 schedule.push(Step::Attention {
3610 params: p,
3611 mask_buf,
3612 });
3613 if let Some((tmp_byte, bytes)) = attn_scratch_copy {
3614 schedule.push(Step::BufferCopy {
3615 src_byte_off: tmp_byte,
3616 dst_byte_off: out_byte,
3617 bytes,
3618 });
3619 }
3620 let ak = attention_kernel(&dev.device);
3621 let u = emit_uniform(std::mem::size_of::<AttentionParams>());
3622 let bg = bind_two_buf0_window(&dev.device, ak, &arena.buffer, base, size, &u);
3623 uniforms.push(u);
3624 bind_groups.push(bg);
3625 }
3626
3627 Op::AttentionBackward {
3628 num_heads,
3629 head_dim,
3630 mask_kind,
3631 wrt,
3632 } => {
3633 use rlx_ir::op::AttentionBwdWrt;
3634 let q_id = node.inputs[0];
3635 let k_id = node.inputs[1];
3636 let v_id = node.inputs[2];
3637 let dy_id = node.inputs[3];
3638 let q_shape = graph.node(q_id).shape.dims();
3639 let k_shape = graph.node(k_id).shape.dims();
3640 let hd = *head_dim as u32;
3641 let q_ir = graph.node(q_id).shape.clone();
3642 let k_ir = graph.node(k_id).shape.clone();
3643 let geom = rlx_ir::attention_geom(&q_ir, &k_ir, *num_heads, *head_dim);
3644 let bhsd = geom.bhsd;
3645 let (batch, heads, seq_q, seq_k) = match q_shape.len() {
3646 4 => (
3647 geom.batch as u32,
3648 geom.heads as u32,
3649 geom.seq_q as u32,
3650 geom.seq_k as u32,
3651 ),
3652 3 => {
3653 let h = q_shape[2].unwrap_static() as u32 / hd;
3654 (
3655 q_shape[0].unwrap_static() as u32 / h,
3656 h,
3657 q_shape[1].unwrap_static() as u32,
3658 k_shape[1].unwrap_static() as u32,
3659 )
3660 }
3661 other => panic!(
3662 "rlx-wgpu AttentionBackward: only rank-3/4 Q,K,V (got rank {other})"
3663 ),
3664 };
3665 let scale = 1.0_f32 / (hd as f32).sqrt();
3666 let (mask_kind_id, mask_off, mask_buf, window) = match mask_kind {
3667 MaskKind::None => (0u32, 0u32, None, 0u32),
3668 MaskKind::Causal => (1u32, 0u32, None, 0u32),
3669 MaskKind::Custom => {
3670 (2u32, (arena.offset(node.inputs[4]) / 4) as u32, None, 0u32)
3671 }
3672 MaskKind::Bias => {
3673 (4u32, (arena.offset(node.inputs[4]) / 4) as u32, None, 0u32)
3674 }
3675 MaskKind::SlidingWindow(w) => (3u32, 0u32, None, *w as u32),
3676 };
3677 struct MStrides {
3678 b: u32,
3679 h: u32,
3680 q: u32,
3681 k: u32,
3682 }
3683 let mask_strides = if mask_kind_id == 2 || mask_kind_id == 4 {
3684 let m_dims = graph.node(node.inputs[4]).shape.dims();
3685 let dim = |i: usize| m_dims[i].unwrap_static() as u32;
3686 match m_dims.len() {
3687 2 => MStrides {
3688 b: dim(1),
3689 h: 0,
3690 q: 0,
3691 k: 1,
3692 },
3693 3 => MStrides {
3694 b: dim(1) * dim(2),
3695 h: 0,
3696 q: dim(2),
3697 k: 1,
3698 },
3699 4 => MStrides {
3700 b: dim(1) * dim(2) * dim(3),
3701 h: dim(2) * dim(3),
3702 q: dim(3),
3703 k: 1,
3704 },
3705 _ => MStrides {
3706 b: heads * seq_q * seq_k,
3707 h: seq_q * seq_k,
3708 q: seq_k,
3709 k: 1,
3710 },
3711 }
3712 } else {
3713 MStrides {
3714 b: heads * seq_q * seq_k,
3715 h: seq_q * seq_k,
3716 q: seq_k,
3717 k: 1,
3718 }
3719 };
3720 let stride = |shape: &[rlx_ir::shape::Dim], seq_extent: u32| {
3721 rlx_ir::strides_for_shape(shape, heads, hd, seq_extent, bhsd)
3722 };
3723 let (q_b, q_h, q_s) = stride(q_shape, seq_q);
3724 let (k_b, k_h, k_s) = stride(k_shape, seq_k);
3725 let v_shape = graph.node(v_id).shape.dims();
3726 let (v_b, v_h, v_s) = stride(v_shape, seq_k);
3727 let out_shape = node.shape.dims();
3728 let out_seq = match wrt {
3729 AttentionBwdWrt::Query => seq_q,
3730 AttentionBwdWrt::Key | AttentionBwdWrt::Value => seq_k,
3731 };
3732 let (o_b, o_h, o_s) = stride(out_shape, out_seq);
3733 let wrt_id = match wrt {
3734 AttentionBwdWrt::Query => 0u32,
3735 AttentionBwdWrt::Key => 1u32,
3736 AttentionBwdWrt::Value => 2u32,
3737 };
3738 let p = AttentionBwdParams {
3739 batch,
3740 heads,
3741 seq_q,
3742 seq_k,
3743 head_dim: hd,
3744 q_off: (arena.offset(q_id) / 4) as u32,
3745 k_off: (arena.offset(k_id) / 4) as u32,
3746 v_off: (arena.offset(v_id) / 4) as u32,
3747 dy_off: (arena.offset(dy_id) / 4) as u32,
3748 out_off: (arena.offset(node.id) / 4) as u32,
3749 mask_off,
3750 mask_kind: mask_kind_id,
3751 scale_bits: scale.to_bits(),
3752 window,
3753 wrt: wrt_id,
3754 seq_q_stride: mask_strides.q,
3755 seq_k_stride: mask_strides.k,
3756 mask_batch_stride: mask_strides.b,
3757 mask_head_stride: mask_strides.h,
3758 _pad_mask_0: 0,
3759 _pad_mask_1: 0,
3760 _pad_mask_2: 0,
3761 q_batch_stride: q_b,
3762 q_head_stride: q_h,
3763 q_seq_stride: q_s,
3764 _pad_q: 0,
3765 k_batch_stride: k_b,
3766 k_head_stride: k_h,
3767 k_seq_stride: k_s,
3768 _pad_k: 0,
3769 v_batch_stride: v_b,
3770 v_head_stride: v_h,
3771 v_seq_stride: v_s,
3772 _pad_v: 0,
3773 o_batch_stride: o_b,
3774 o_head_stride: o_h,
3775 o_seq_stride: o_s,
3776 _pad_o: 0,
3777 };
3778 schedule.push(Step::AttentionBackward {
3779 params: p,
3780 mask_buf,
3781 });
3782 let ak = attention_bwd_kernel(&dev.device);
3783 let u = emit_uniform(std::mem::size_of::<AttentionBwdParams>());
3784 let bg = bind_op_output_window(&dev.device, ak, &arena, node.id, &u);
3785 uniforms.push(u);
3786 bind_groups.push(bg);
3787 }
3788
3789 Op::Rope {
3790 head_dim,
3791 n_rot: _,
3792 style,
3793 } => {
3794 let x_id = node.inputs[0];
3795 let cos_id = node.inputs[1];
3796 let sin_id = node.inputs[2];
3797 let x_shape = graph.node(x_id).shape.dims();
3798 let last = x_shape.last().map(|d| d.unwrap_static()).unwrap_or(0);
3799 if !last.is_multiple_of(*head_dim) {
3800 panic!(
3801 "rlx-wgpu Rope: last_dim ({last}) must be a multiple \
3802 of head_dim ({head_dim})"
3803 );
3804 }
3805 if head_dim % 2 != 0 {
3806 panic!("rlx-wgpu Rope: head_dim must be even");
3807 }
3808 let total: u32 = x_shape.iter().map(|d| d.unwrap_static() as u32).product();
3809 let seq = x_shape[x_shape.len() - 2].unwrap_static() as u32;
3810 let batch = total / (seq * last as u32).max(1);
3815 let cos_is_param = tensor_is_graph_param(&graph, ¶m_offsets, cos_id);
3816 let cos_bytes = arena.len_of(cos_id) as u64;
3817 let rope_win: Vec<NodeId> = if cos_is_param && cos_bytes > ARENA_STAGE_CAP {
3818 vec![cos_id, sin_id, node.id, x_id]
3819 } else {
3820 vec![node.id, x_id, cos_id, sin_id]
3821 };
3822 let mut scratch = arena.scratch_off as u64;
3823 let (mut base, mut size, param_anchor) = arena_multi_op_window(
3824 &dev.device,
3825 &arena,
3826 &graph,
3827 ¶m_offsets,
3828 &mut schedule,
3829 &mut scratch,
3830 &rope_win,
3831 );
3832 if !param_anchor {
3833 base = arena_bind_window_covering_scratch_if_needed(
3834 &arena, base, size, scratch,
3835 );
3836 }
3837 let in_off = arena_off_in_bind_window(
3838 &graph,
3839 ¶m_offsets,
3840 &dev.device,
3841 &arena,
3842 &mut schedule,
3843 &mut scratch,
3844 x_id,
3845 &mut base,
3846 &mut size,
3847 );
3848 let cos_off = arena_off_in_bind_window(
3849 &graph,
3850 ¶m_offsets,
3851 &dev.device,
3852 &arena,
3853 &mut schedule,
3854 &mut scratch,
3855 cos_id,
3856 &mut base,
3857 &mut size,
3858 );
3859 let sin_off = arena_off_in_bind_window(
3860 &graph,
3861 ¶m_offsets,
3862 &dev.device,
3863 &arena,
3864 &mut schedule,
3865 &mut scratch,
3866 sin_id,
3867 &mut base,
3868 &mut size,
3869 );
3870 let p = RopeParams {
3871 n_total: total,
3872 seq,
3873 head_dim: *head_dim as u32,
3874 half: (*head_dim / 2) as u32,
3875 in_off,
3876 cos_off,
3877 sin_off,
3878 out_off: arena_local_off_f32(&arena, node.id, base),
3879 last_dim: last as u32,
3880 batch,
3881 seq_stride: seq,
3882 style: match style {
3883 rlx_ir::op::RopeStyle::NeoX => 0,
3884 rlx_ir::op::RopeStyle::GptJ => 1,
3885 },
3886 };
3887 schedule.push(Step::Rope { params: p });
3888 let rk = rope_kernel(&dev.device);
3889 let u = emit_uniform(std::mem::size_of::<RopeParams>());
3890 let bg = bind_two_buf0_window(&dev.device, rk, &arena.buffer, base, size, &u);
3891 uniforms.push(u);
3892 bind_groups.push(bg);
3893 }
3894
3895 Op::Expand { target_shape } => {
3896 let in_id = node.inputs[0];
3897 let in_shape = graph.node(in_id).shape.dims();
3898 let in_rank = in_shape.len();
3899 let rank = target_shape.len();
3900 if in_rank > rank {
3901 panic!(
3902 "rlx-wgpu Expand: rank mismatch \
3903 (in_rank={in_rank}, target_rank={rank})"
3904 );
3905 }
3906 let pad = rank.saturating_sub(in_rank);
3909 let out_dims: Vec<u32> = target_shape.iter().map(|&d| d as u32).collect();
3910 let in_dims: Vec<u32> = (0..rank)
3911 .map(|i| {
3912 if i < pad {
3913 1
3914 } else {
3915 in_shape[i - pad].unwrap_static() as u32
3916 }
3917 })
3918 .collect();
3919 let mut in_strides_row = vec![1u32; rank];
3923 for i in (0..rank.saturating_sub(1)).rev() {
3924 in_strides_row[i] = in_strides_row[i + 1] * in_dims[i + 1];
3925 }
3926 let strides_for_out: Vec<u32> = (0..rank)
3927 .map(|i| {
3928 if in_dims[i] == 1 && out_dims[i] != 1 {
3929 0
3930 } else {
3931 in_strides_row[i]
3932 }
3933 })
3934 .collect();
3935
3936 let mut meta_data: Vec<u32> = Vec::with_capacity(rank * 2);
3937 meta_data.extend_from_slice(&out_dims);
3938 meta_data.extend_from_slice(&strides_for_out);
3939 let meta_buf = dev.device.create_buffer(&wgpu::BufferDescriptor {
3940 label: Some("rlx-wgpu expand meta"),
3941 size: (meta_data.len() * 4).max(4) as u64,
3942 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
3943 mapped_at_creation: false,
3944 });
3945 dev.queue
3946 .write_buffer(&meta_buf, 0, bytemuck::cast_slice(&meta_data));
3947 let meta_idx = meta_buffers.len();
3948 meta_buffers.push(meta_buf);
3949
3950 let bucket_outermost = if in_dims[0] == out_dims[0] {
3956 1u32
3957 } else {
3958 0u32
3959 };
3960 let exp_ids = [node.id, in_id];
3961 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
3962 let exp_fits = arena_span_bytes(&arena, &exp_ids) <= max_binding;
3963 let mut scratch = arena.scratch_off as u64;
3964 let (mut base, mut size, param_anchor) = arena_multi_op_window(
3965 &dev.device,
3966 &arena,
3967 &graph,
3968 ¶m_offsets,
3969 &mut schedule,
3970 &mut scratch,
3971 &exp_ids,
3972 );
3973 if !exp_fits && !param_anchor {
3974 base = arena_bind_window_covering_scratch_if_needed(
3975 &arena, base, size, scratch,
3976 );
3977 }
3978 let in_off = arena_off_in_bind_window(
3979 &graph,
3980 ¶m_offsets,
3981 &dev.device,
3982 &arena,
3983 &mut schedule,
3984 &mut scratch,
3985 in_id,
3986 &mut base,
3987 &mut size,
3988 );
3989 let out_off = arena_off_in_bind_window(
3990 &graph,
3991 ¶m_offsets,
3992 &dev.device,
3993 &arena,
3994 &mut schedule,
3995 &mut scratch,
3996 node.id,
3997 &mut base,
3998 &mut size,
3999 );
4000 let p = ExpandParams {
4001 rank: rank as u32,
4002 out_total: elems,
4003 in_off,
4004 out_off,
4005 bucket_outermost,
4006 out_dim_0: out_dims[0],
4007 _p2: 0,
4008 _p3: 0,
4009 };
4010 schedule.push(Step::Expand {
4011 params: p,
4012 meta_idx,
4013 });
4014 let ek = expand_kernel(&dev.device);
4015 let u = emit_uniform(std::mem::size_of::<ExpandParams>());
4016 let bg = dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
4017 label: Some("rlx-wgpu expand bg"),
4018 layout: &ek.bgl,
4019 entries: &[
4020 wgpu::BindGroupEntry {
4021 binding: 0,
4022 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
4023 buffer: &arena.buffer,
4024 offset: base,
4025 size: NonZeroU64::new(size),
4026 }),
4027 },
4028 wgpu::BindGroupEntry {
4029 binding: 1,
4030 resource: u.as_entire_binding(),
4031 },
4032 wgpu::BindGroupEntry {
4033 binding: 2,
4034 resource: meta_buffers[meta_idx].as_entire_binding(),
4035 },
4036 ],
4037 });
4038 uniforms.push(u);
4039 bind_groups.push(bg);
4040 }
4041
4042 Op::Gather { axis } => {
4043 let table_id = node.inputs[0];
4044 let idx_id = node.inputs[1];
4045 let table_is_param = tensor_is_graph_param(&graph, ¶m_offsets, table_id);
4046 let table_bytes = arena.len_of(table_id) as u64;
4047 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
4056 let gather_needs_split = *axis == 0
4057 && arena_whole_arena_bind(&arena, max_binding).is_none()
4058 && arena_span_bytes(&arena, &[table_id, idx_id, node.id]) > max_binding;
4059 if gather_needs_split {
4060 let table_shape = graph.node(table_id).shape.dims();
4061 let idx_shape = graph.node(idx_id).shape.dims();
4062 let vocab = table_shape[0].unwrap_static() as u32;
4063 let dim: u32 = table_shape[1..]
4064 .iter()
4065 .map(|d| d.unwrap_static() as u32)
4066 .product::<u32>()
4067 .max(1);
4068 let n_idx: u32 =
4069 idx_shape.iter().map(|d| d.unwrap_static() as u32).product();
4070 let table_w = arena.len_of(table_id) as u64;
4071 assert!(
4072 table_w <= max_binding,
4073 "rlx-wgpu gather_split: embedding table {table_w} bytes exceeds \
4074 max_storage_buffer_binding_size {max_binding}"
4075 );
4076 schedule.push(Step::GatherSplit {
4077 n_out: elems,
4078 n_idx,
4079 dim,
4080 vocab,
4081 table_byte_off: arena.offset(table_id) as u64,
4082 idx_byte_off: arena.offset(idx_id) as u64,
4083 out_byte_off: arena.offset(node.id) as u64,
4084 });
4085 continue;
4089 }
4090 let gather_win: Vec<NodeId> = if table_is_param && table_bytes > ARENA_STAGE_CAP
4091 {
4092 vec![table_id, node.id, idx_id]
4093 } else {
4094 vec![node.id, idx_id, table_id]
4095 };
4096 let mut scratch = arena.scratch_off as u64;
4097 let (mut base, mut size, table_anchor) = arena_multi_op_window(
4098 &dev.device,
4099 &arena,
4100 &graph,
4101 ¶m_offsets,
4102 &mut schedule,
4103 &mut scratch,
4104 &gather_win,
4105 );
4106 if !table_anchor {
4107 base = arena_bind_window_covering_scratch_if_needed(
4108 &arena, base, size, scratch,
4109 );
4110 }
4111 let in_off =
4112 if table_anchor && arena_tensor_in_window(&arena, table_id, base, size) {
4113 arena_local_off_f32(&arena, table_id, base)
4114 } else {
4115 arena_off_in_bind_window(
4116 &graph,
4117 ¶m_offsets,
4118 &dev.device,
4119 &arena,
4120 &mut schedule,
4121 &mut scratch,
4122 table_id,
4123 &mut base,
4124 &mut size,
4125 )
4126 };
4127 let idx_off = arena_off_in_bind_window(
4128 &graph,
4129 ¶m_offsets,
4130 &dev.device,
4131 &arena,
4132 &mut schedule,
4133 &mut scratch,
4134 idx_id,
4135 &mut base,
4136 &mut size,
4137 );
4138 let out_off = arena_local_off_f32(&arena, node.id, base);
4139 if *axis == 0 {
4140 let table_shape = graph.node(table_id).shape.dims();
4141 let idx_shape = graph.node(idx_id).shape.dims();
4142 let vocab = table_shape[0].unwrap_static() as u32;
4143 let dim: u32 = table_shape[1..]
4144 .iter()
4145 .map(|d| d.unwrap_static() as u32)
4146 .product::<u32>()
4147 .max(1);
4148 let n_idx: u32 =
4149 idx_shape.iter().map(|d| d.unwrap_static() as u32).product();
4150 let p = GatherParams {
4151 n_out: elems,
4152 n_idx,
4153 dim,
4154 vocab,
4155 in_off,
4156 idx_off,
4157 out_off,
4158 _p0: 0,
4159 };
4160 schedule.push(Step::Gather { params: p });
4161 let gk = gather_kernel(&dev.device);
4162 let u = emit_uniform(std::mem::size_of::<GatherParams>());
4163 let bg =
4164 bind_two_buf0_window(&dev.device, gk, &arena.buffer, base, size, &u);
4165 uniforms.push(u);
4166 bind_groups.push(bg);
4167 } else {
4168 let table_shape = graph.node(table_id).shape.dims();
4169 let idx_shape = graph.node(idx_id).shape.dims();
4170 let outer: u32 = table_shape[..*axis]
4171 .iter()
4172 .map(|d| d.unwrap_static() as u32)
4173 .product::<u32>()
4174 .max(1);
4175 let trailing: u32 = table_shape[*axis + 1..]
4176 .iter()
4177 .map(|d| d.unwrap_static() as u32)
4178 .product::<u32>()
4179 .max(1);
4180 let axis_dim = table_shape[*axis].unwrap_static() as u32;
4181 let num_idx: u32 =
4182 idx_shape.iter().map(|d| d.unwrap_static() as u32).product();
4183 let total = outer * num_idx * trailing;
4184 let p = GatherAxisParams {
4185 total,
4186 outer,
4187 axis_dim,
4188 num_idx,
4189 trailing,
4190 table_off: in_off,
4191 idx_off,
4192 out_off,
4193 };
4194 schedule.push(Step::GatherAxis { params: p });
4195 let gk = gather_axis_kernel(&dev.device);
4196 let u = emit_uniform(std::mem::size_of::<GatherAxisParams>());
4197 let bg =
4198 bind_two_buf0_window(&dev.device, gk, &arena.buffer, base, size, &u);
4199 uniforms.push(u);
4200 bind_groups.push(bg);
4201 }
4202 }
4203
4204 Op::FusedMatMulBiasAct { activation } => {
4205 let a_id = node.inputs[0];
4208 let b_id = node.inputs[1];
4209 let bias_id = node.inputs[2];
4210 let a_shape = graph.node(a_id).shape.dims();
4211 let b_shape = graph.node(b_id).shape.dims();
4212 let out_shape = node.shape.dims();
4213 let (m, k, n) =
4214 if a_shape.len() == 2 && b_shape.len() == 2 && out_shape.len() == 2 {
4215 (
4216 a_shape[0].unwrap_static() as u32,
4217 a_shape[1].unwrap_static() as u32,
4218 b_shape[1].unwrap_static() as u32,
4219 )
4220 } else if a_shape.len() >= 2
4221 && b_shape.len() == 2
4222 && out_shape.len() == a_shape.len()
4223 {
4224 let leading: usize = a_shape[..a_shape.len() - 2]
4225 .iter()
4226 .map(|d| d.unwrap_static())
4227 .product();
4228 let m_inner = a_shape[a_shape.len() - 2].unwrap_static();
4229 let k_inner = a_shape[a_shape.len() - 1].unwrap_static();
4230 let n_inner = b_shape[1].unwrap_static();
4231 ((leading * m_inner) as u32, k_inner as u32, n_inner as u32)
4232 } else {
4233 panic!(
4234 "rlx-wgpu FusedMatMulBiasAct: unsupported shapes \
4235 a={a_shape:?} b={b_shape:?}"
4236 );
4237 };
4238 let act_id = match activation {
4239 None => 0xFFFFu32,
4240 Some(a) => activation_op_id(*a),
4241 };
4242 let b_is_param = tensor_is_graph_param(&graph, ¶m_offsets, b_id);
4243 let b_bytes = arena.len_of(b_id) as u64;
4244 let mut compute_precision = derive_matmul_compute(
4245 &dev.device,
4246 &graph,
4247 &coop_f16_vk_mirror_acts,
4248 a_id,
4249 b_id,
4250 m,
4251 k,
4252 n,
4253 );
4254 if b_is_param && b_bytes > ARENA_STAGE_CAP && arena.param_fits_f16_mirror(b_id)
4255 {
4256 compute_precision = MatmulCompute::F16;
4257 }
4258
4259 let mqk_eligible = act_id == 0xFFFFu32
4263 && matches!(
4264 compute_precision,
4265 MatmulCompute::F32 | MatmulCompute::CoopF32 | MatmulCompute::CoopF16Vk
4266 );
4267 if mqk_eligible && let Some(&(q_id, k_id_n, v_id)) = qkv_split.get(&node.id) {
4268 let head_width = n / 3;
4269 let qkv_kind = match compute_precision {
4270 MatmulCompute::CoopF16Vk => MatmulQkvKind::CoopF16Vk,
4271 MatmulCompute::CoopF32 => MatmulQkvKind::CoopF32,
4272 _ => MatmulQkvKind::F32,
4273 };
4274 let b_in_arena = !matmul_b_from_f16(compute_precision, b_is_param);
4275 let (mut base, mut size, param_anchor) = arena_matmul_bind_window(
4276 &dev.device,
4277 &arena,
4278 &graph,
4279 ¶m_offsets,
4280 q_id,
4281 a_id,
4282 b_id,
4283 b_in_arena,
4284 );
4285 let mut scratch = arena.scratch_off as u64;
4286 if param_anchor {
4287 arena_ensure_scratch_in_window(&mut scratch, base, size);
4288 }
4289 if b_is_param && b_bytes > ARENA_STAGE_CAP && b_in_arena {
4290 assert!(
4291 param_anchor && arena_tensor_in_window(&arena, b_id, base, size),
4292 "rlx-wgpu FusedMatMul QKV: large param B {:?} not in bind window",
4293 b_id,
4294 );
4295 }
4296 let a_off = arena_off_in_bind_window(
4297 &graph,
4298 ¶m_offsets,
4299 &dev.device,
4300 &arena,
4301 &mut schedule,
4302 &mut scratch,
4303 a_id,
4304 &mut base,
4305 &mut size,
4306 );
4307 let q_off = arena_off_in_bind_window(
4308 &graph,
4309 ¶m_offsets,
4310 &dev.device,
4311 &arena,
4312 &mut schedule,
4313 &mut scratch,
4314 q_id,
4315 &mut base,
4316 &mut size,
4317 );
4318 let k_off = arena_off_in_bind_window(
4319 &graph,
4320 ¶m_offsets,
4321 &dev.device,
4322 &arena,
4323 &mut schedule,
4324 &mut scratch,
4325 k_id_n,
4326 &mut base,
4327 &mut size,
4328 );
4329 let v_off = arena_off_in_bind_window(
4330 &graph,
4331 ¶m_offsets,
4332 &dev.device,
4333 &arena,
4334 &mut schedule,
4335 &mut scratch,
4336 v_id,
4337 &mut base,
4338 &mut size,
4339 );
4340 let bias_off = arena_off_in_bind_window(
4341 &graph,
4342 ¶m_offsets,
4343 &dev.device,
4344 &arena,
4345 &mut schedule,
4346 &mut scratch,
4347 bias_id,
4348 &mut base,
4349 &mut size,
4350 );
4351 let b_off_f32 = if !b_in_arena {
4352 (arena.offset(b_id) / 4) as u32
4353 } else if b_is_param
4354 && b_bytes > ARENA_STAGE_CAP
4355 && arena_tensor_in_window(&arena, b_id, base, size)
4356 {
4357 arena_local_off_f32(&arena, b_id, base)
4358 } else {
4359 arena_off_in_bind_window(
4360 &graph,
4361 ¶m_offsets,
4362 &dev.device,
4363 &arena,
4364 &mut schedule,
4365 &mut scratch,
4366 b_id,
4367 &mut base,
4368 &mut size,
4369 )
4370 };
4371 let b_off_global = (arena.offset(b_id) / 4) as u32;
4372 maybe_push_coop_f16_vk_casts(
4373 &graph,
4374 a_id,
4375 b_id,
4376 &coop_f16_vk_mirror_acts,
4377 &dev.device,
4378 &arena,
4379 &mut schedule,
4380 &mut uniforms,
4381 &mut bind_groups,
4382 &mm_cast,
4383 compute_precision,
4384 a_off,
4385 m,
4386 k,
4387 1,
4388 if qkv_kind == MatmulQkvKind::CoopF16Vk {
4389 b_off_global
4390 } else {
4391 b_off_f32
4392 },
4393 n,
4394 );
4395 let p = MatmulQkvParams {
4396 m,
4397 k,
4398 n,
4399 a_off,
4400 b_off: if qkv_kind == MatmulQkvKind::CoopF16Vk {
4401 b_off_global
4402 } else {
4403 b_off_f32
4404 },
4405 q_off,
4406 k_off,
4407 v_off,
4408 head_width,
4409 has_bias: 1,
4410 bias_off,
4411 _p0: 0,
4412 _p1: 0,
4413 _p2: 0,
4414 _p3: 0,
4415 _p4: 0,
4416 };
4417 schedule.push(Step::MatmulQkv {
4418 params: p,
4419 kind: qkv_kind,
4420 });
4421 register_coop_f16_vk_b_param(
4422 &mut coop_f16_b_param,
4423 ¶m_offsets,
4424 b_id,
4425 p.b_off,
4426 match qkv_kind {
4427 MatmulQkvKind::CoopF16Vk => MatmulCompute::CoopF16Vk,
4428 MatmulQkvKind::CoopF32 => MatmulCompute::CoopF32,
4429 MatmulQkvKind::F32 => MatmulCompute::F32,
4430 },
4431 );
4432 let u = emit_uniform(std::mem::size_of::<MatmulQkvParams>());
4433 let bg = match qkv_kind {
4434 MatmulQkvKind::CoopF16Vk => {
4435 let mqk = matmul_qkv_coop_f16_vk_kernel(&dev.device).expect(
4436 "coop f16 matmul_qkv kernel: feature was checked but missing",
4437 );
4438 let (bg, b_off_adj) = build_matmul_qkv_coop_f16_vk_bind_group(
4439 &dev.device,
4440 mqk,
4441 &arena,
4442 base,
4443 size,
4444 &u,
4445 k,
4446 n,
4447 p.b_off,
4448 );
4449 if let Some(Step::MatmulQkv { params, .. }) = schedule.last_mut() {
4450 params.b_off = b_off_adj;
4451 }
4452 bg
4453 }
4454 MatmulQkvKind::CoopF32 => bind_two_buf0_window(
4455 &dev.device,
4456 matmul_qkv_coop_f32_kernel(&dev.device).expect(
4457 "coop matmul_qkv kernel: hardware feature was checked but kernel missing",
4458 ),
4459 &arena.buffer,
4460 base,
4461 size,
4462 &u,
4463 ),
4464 MatmulQkvKind::F32 => bind_two_buf0_window(
4465 &dev.device,
4466 matmul_qkv_kernel(&dev.device),
4467 &arena.buffer,
4468 base,
4469 size,
4470 &u,
4471 ),
4472 };
4473 uniforms.push(u);
4474 bind_groups.push(bg);
4475 if qkv_kind == MatmulQkvKind::CoopF16Vk {
4476 coop_f16_vk_wide_bind_groups.insert(
4477 schedule.len() - 1,
4478 bind_two_buf0_window(
4479 &dev.device,
4480 matmul_qkv_kernel(&dev.device),
4481 &arena.buffer,
4482 base,
4483 size,
4484 &uniforms[uniforms.len() - 1],
4485 ),
4486 );
4487 }
4488 } else {
4489 let b_in_arena = !matmul_b_from_f16(compute_precision, b_is_param);
4490 let (mut base, mut size, param_anchor) = arena_matmul_bind_window(
4491 &dev.device,
4492 &arena,
4493 &graph,
4494 ¶m_offsets,
4495 node.id,
4496 a_id,
4497 b_id,
4498 b_in_arena,
4499 );
4500 let mut scratch = arena.scratch_off as u64;
4501 if param_anchor {
4502 arena_ensure_scratch_in_window(&mut scratch, base, size);
4503 }
4504 if b_is_param && b_bytes > ARENA_STAGE_CAP && b_in_arena {
4505 assert!(
4506 param_anchor && arena_tensor_in_window(&arena, b_id, base, size),
4507 "rlx-wgpu FusedMatMul: large param B {:?} not in bind window",
4508 b_id,
4509 );
4510 }
4511 let a_off_f32 = arena_off_in_bind_window(
4512 &graph,
4513 ¶m_offsets,
4514 &dev.device,
4515 &arena,
4516 &mut schedule,
4517 &mut scratch,
4518 a_id,
4519 &mut base,
4520 &mut size,
4521 );
4522 let b_off_f32 = if !b_in_arena {
4523 (arena.offset(b_id) / 4) as u32
4524 } else if b_is_param
4525 && b_bytes > ARENA_STAGE_CAP
4526 && arena_tensor_in_window(&arena, b_id, base, size)
4527 {
4528 arena_local_off_f32(&arena, b_id, base)
4529 } else {
4530 arena_off_in_bind_window(
4531 &graph,
4532 ¶m_offsets,
4533 &dev.device,
4534 &arena,
4535 &mut schedule,
4536 &mut scratch,
4537 b_id,
4538 &mut base,
4539 &mut size,
4540 )
4541 };
4542 let bias_off_f32 = arena_off_in_bind_window(
4543 &graph,
4544 ¶m_offsets,
4545 &dev.device,
4546 &arena,
4547 &mut schedule,
4548 &mut scratch,
4549 bias_id,
4550 &mut base,
4551 &mut size,
4552 );
4553 let b_off_global = (arena.offset(b_id) / 4) as u32;
4554 let b_off_bind = if b_is_param
4555 && matches!(
4556 compute_precision,
4557 MatmulCompute::Coop16
4558 | MatmulCompute::CoopF16Vk
4559 | MatmulCompute::F16
4560 ) {
4561 b_off_global
4562 } else {
4563 b_off_f32
4564 };
4565 maybe_push_coop_f16_vk_casts(
4566 &graph,
4567 a_id,
4568 b_id,
4569 &coop_f16_vk_mirror_acts,
4570 &dev.device,
4571 &arena,
4572 &mut schedule,
4573 &mut uniforms,
4574 &mut bind_groups,
4575 &mm_cast,
4576 compute_precision,
4577 a_off_f32,
4578 m,
4579 k,
4580 1,
4581 b_off_bind,
4582 n,
4583 );
4584 schedule.push(Step::Matmul {
4585 m,
4586 k,
4587 n,
4588 batch: 1,
4589 a_batch_stride: 0,
4590 b_batch_stride: 0,
4591 c_batch_stride: 0,
4592 a_off_f32,
4593 b_off_f32,
4594 c_off_f32: arena_local_off_f32(&arena, node.id, base),
4595 has_bias: 1,
4596 bias_off_f32,
4597 act_id,
4598 b_is_param,
4599 compute_precision,
4600 });
4601 register_coop_f16_vk_b_param(
4602 &mut coop_f16_b_param,
4603 ¶m_offsets,
4604 b_id,
4605 b_off_bind,
4606 compute_precision,
4607 );
4608 let u = emit_uniform(std::mem::size_of::<MatmulParams>());
4609 let (bg, b_off_adj) = build_matmul_bind_group(
4610 &dev.device,
4611 mm_k,
4612 mm_w,
4613 &mm_f16w,
4614 &mm_f16c,
4615 &mm_coop,
4616 &mm_coop_f32,
4617 &arena,
4618 base,
4619 size,
4620 &u,
4621 b_is_param,
4622 compute_precision,
4623 k,
4624 n,
4625 1,
4626 b_off_bind,
4627 0,
4628 );
4629 if let Some(Step::Matmul { b_off_f32, .. }) = schedule.last_mut() {
4630 *b_off_f32 = b_off_adj;
4631 }
4632 uniforms.push(u);
4633 bind_groups.push(bg);
4634 if compute_precision == MatmulCompute::CoopF16Vk {
4635 coop_f16_vk_wide_bind_groups.insert(
4636 schedule.len() - 1,
4637 bind_two_buf0_window(
4638 &dev.device,
4639 mm_w_active_compile,
4640 &arena.buffer,
4641 base,
4642 size,
4643 &uniforms[uniforms.len() - 1],
4644 ),
4645 );
4646 }
4647 }
4648 }
4649
4650 Op::DotGeneral { .. } => {
4651 panic!(
4656 "rlx-wgpu DotGeneral: leaked past unfusion pass — \
4657 check unfuse.rs::expand_dot_general for missing patterns"
4658 );
4659 }
4660
4661 Op::Sample {
4662 top_k,
4663 top_p,
4664 temperature,
4665 seed,
4666 } => {
4667 let in_id = node.inputs[0];
4668 let in_shape = graph.node(in_id).shape.dims();
4669 let inner = in_shape[in_shape.len() - 1].unwrap_static() as u32;
4670 let total: u32 = in_shape.iter().map(|d| d.unwrap_static() as u32).product();
4671 let outer = total / inner.max(1);
4672 let is_greedy = *top_k == 0
4675 && (*top_p - 1.0).abs() < 1e-6
4676 && (*temperature - 1.0).abs() < 1e-6;
4677 if is_greedy {
4678 let p = ArgmaxParams {
4679 outer,
4680 inner,
4681 in_off: (arena.offset(in_id) / 4) as u32,
4682 out_off: (arena.offset(node.id) / 4) as u32,
4683 _p0: 0,
4684 _p1: 0,
4685 _p2: 0,
4686 _p3: 0,
4687 };
4688 schedule.push(Step::Argmax { params: p });
4689 let amk = argmax_kernel(&dev.device);
4690 let u = emit_uniform(std::mem::size_of::<ArgmaxParams>());
4691 let bg = bind_op_output_window(&dev.device, amk, &arena, node.id, &u);
4692 uniforms.push(u);
4693 bind_groups.push(bg);
4694 } else {
4695 let p = SampleParams {
4696 outer,
4697 inner,
4698 in_off: (arena.offset(in_id) / 4) as u32,
4699 out_off: (arena.offset(node.id) / 4) as u32,
4700 top_k: *top_k as u32,
4701 top_p_bits: top_p.to_bits(),
4702 temp_bits: temperature.to_bits(),
4703 seed_lo: *seed as u32,
4704 seed_hi: (*seed >> 32) as u32,
4705 _p0: 0,
4706 _p1: 0,
4707 _p2: 0,
4708 };
4709 schedule.push(Step::Sample { params: p });
4710 let sk = sample_kernel(&dev.device);
4711 let u = emit_uniform(std::mem::size_of::<SampleParams>());
4712 let bg = bind_op_output_window(&dev.device, sk, &arena, node.id, &u);
4713 uniforms.push(u);
4714 bind_groups.push(bg);
4715 }
4716 }
4717
4718 Op::Pool {
4719 kind,
4720 kernel_size,
4721 stride,
4722 padding,
4723 } => {
4724 let in_shape = graph.node(node.inputs[0]).shape.dims();
4725 let out_shape = node.shape.dims();
4726 let op_id: u32 = match kind {
4727 ReduceOp::Sum => 0,
4728 ReduceOp::Mean => 1,
4729 ReduceOp::Max => 2,
4730 ReduceOp::Min => 3,
4731 ReduceOp::Prod => 4,
4732 };
4733 match (kernel_size.len(), in_shape.len(), out_shape.len()) {
4734 (1, 3, 3) => {
4735 let p = Pool1dParams {
4736 n: in_shape[0].unwrap_static() as u32,
4737 c: in_shape[1].unwrap_static() as u32,
4738 l: in_shape[2].unwrap_static() as u32,
4739 l_out: out_shape[2].unwrap_static() as u32,
4740 kl: kernel_size[0] as u32,
4741 sl: stride.first().copied().unwrap_or(1) as u32,
4742 pl: padding.first().copied().unwrap_or(0) as u32,
4743 op: op_id,
4744 in_off: (arena.offset(node.inputs[0]) / 4) as u32,
4745 out_off: (arena.offset(node.id) / 4) as u32,
4746 _p0: 0,
4747 _p1: 0,
4748 _p2: 0,
4749 _p3: 0,
4750 _p4: 0,
4751 _p5: 0,
4752 };
4753 schedule.push(Step::Pool1d { params: p });
4754 let pk = pool1d_kernel(&dev.device);
4755 let u = emit_uniform(std::mem::size_of::<Pool1dParams>());
4756 let bg = bind_op_output_window(&dev.device, pk, &arena, node.id, &u);
4757 uniforms.push(u);
4758 bind_groups.push(bg);
4759 }
4760 (2, 4, 4) => {
4761 let p = Pool2dParams {
4762 n: in_shape[0].unwrap_static() as u32,
4763 c: in_shape[1].unwrap_static() as u32,
4764 h: in_shape[2].unwrap_static() as u32,
4765 w: in_shape[3].unwrap_static() as u32,
4766 h_out: out_shape[2].unwrap_static() as u32,
4767 w_out: out_shape[3].unwrap_static() as u32,
4768 kh: kernel_size[0] as u32,
4769 kw: kernel_size[1] as u32,
4770 sh: stride.first().copied().unwrap_or(1) as u32,
4771 sw: stride.get(1).copied().unwrap_or(1) as u32,
4772 ph: padding.first().copied().unwrap_or(0) as u32,
4773 pw: padding.get(1).copied().unwrap_or(0) as u32,
4774 op: op_id,
4775 in_off: (arena.offset(node.inputs[0]) / 4) as u32,
4776 out_off: (arena.offset(node.id) / 4) as u32,
4777 _p0: 0,
4778 _p1: 0,
4779 _p2: 0,
4780 };
4781 schedule.push(Step::Pool2d { params: p });
4782 let pk = pool2d_kernel(&dev.device);
4783 let u = emit_uniform(std::mem::size_of::<Pool2dParams>());
4784 let bg = bind_op_output_window(&dev.device, pk, &arena, node.id, &u);
4785 uniforms.push(u);
4786 bind_groups.push(bg);
4787 }
4788 (3, 5, 5) => {
4789 let p = Pool3dParams {
4790 n: in_shape[0].unwrap_static() as u32,
4791 c: in_shape[1].unwrap_static() as u32,
4792 d: in_shape[2].unwrap_static() as u32,
4793 h: in_shape[3].unwrap_static() as u32,
4794 w: in_shape[4].unwrap_static() as u32,
4795 d_out: out_shape[2].unwrap_static() as u32,
4796 h_out: out_shape[3].unwrap_static() as u32,
4797 w_out: out_shape[4].unwrap_static() as u32,
4798 kd: kernel_size[0] as u32,
4799 kh: kernel_size[1] as u32,
4800 kw: kernel_size[2] as u32,
4801 sd: stride.first().copied().unwrap_or(1) as u32,
4802 sh: stride.get(1).copied().unwrap_or(1) as u32,
4803 sw: stride.get(2).copied().unwrap_or(1) as u32,
4804 pd: padding.first().copied().unwrap_or(0) as u32,
4805 ph: padding.get(1).copied().unwrap_or(0) as u32,
4806 pw: padding.get(2).copied().unwrap_or(0) as u32,
4807 op: op_id,
4808 in_off: (arena.offset(node.inputs[0]) / 4) as u32,
4809 out_off: (arena.offset(node.id) / 4) as u32,
4810 _p0: 0,
4811 _p1: 0,
4812 };
4813 schedule.push(Step::Pool3d { params: p });
4814 let pk = pool3d_kernel(&dev.device);
4815 let u = emit_uniform(std::mem::size_of::<Pool3dParams>());
4816 let bg = bind_op_output_window(&dev.device, pk, &arena, node.id, &u);
4817 uniforms.push(u);
4818 bind_groups.push(bg);
4819 }
4820 (k, n, m) => panic!(
4821 "rlx-wgpu Pool: kernel-rank {k} with input rank {n} / \
4822 output rank {m} not supported (use 1D/2D/3D NCHW)"
4823 ),
4824 }
4825 }
4826
4827 Op::Conv {
4828 kernel_size,
4829 stride,
4830 padding,
4831 dilation,
4832 groups,
4833 } => {
4834 let in_id = node.inputs[0];
4835 let w_id = node.inputs[1];
4836 let win_ids = [node.id, in_id, w_id];
4837 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
4838 let fits = arena_span_bytes(&arena, &win_ids) <= max_binding;
4839 let mut scratch = arena.scratch_off as u64;
4840 let (mut base, mut size, param_anchor) = arena_multi_op_window(
4841 &dev.device,
4842 &arena,
4843 &graph,
4844 ¶m_offsets,
4845 &mut schedule,
4846 &mut scratch,
4847 &win_ids,
4848 );
4849 arena_expand_bind_window(&arena, &win_ids, &mut base, &mut size, max_binding);
4850 if !fits && !param_anchor {
4851 base = arena_bind_window_covering_scratch_if_needed(
4852 &arena, base, size, scratch,
4853 );
4854 }
4855 let in_off = arena_off_in_bind_window(
4856 &graph,
4857 ¶m_offsets,
4858 &dev.device,
4859 &arena,
4860 &mut schedule,
4861 &mut scratch,
4862 in_id,
4863 &mut base,
4864 &mut size,
4865 );
4866 let w_off = arena_off_in_bind_window(
4867 &graph,
4868 ¶m_offsets,
4869 &dev.device,
4870 &arena,
4871 &mut schedule,
4872 &mut scratch,
4873 w_id,
4874 &mut base,
4875 &mut size,
4876 );
4877 let out_off = arena_off_in_bind_window(
4878 &graph,
4879 ¶m_offsets,
4880 &dev.device,
4881 &arena,
4882 &mut schedule,
4883 &mut scratch,
4884 node.id,
4885 &mut base,
4886 &mut size,
4887 );
4888 let in_shape = graph.node(in_id).shape.dims();
4889 let w_shape = graph.node(w_id).shape.dims();
4890 let out_shape = node.shape.dims();
4891 let s = |i: usize| stride.get(i).copied().unwrap_or(1) as u32;
4892 let p = |i: usize| padding.get(i).copied().unwrap_or(0) as u32;
4893 let d = |i: usize| dilation.get(i).copied().unwrap_or(1) as u32;
4894 match (
4895 kernel_size.len(),
4896 in_shape.len(),
4897 w_shape.len(),
4898 out_shape.len(),
4899 ) {
4900 (1, 3, 3, 3) => {
4901 let p1 = Conv1dParams {
4902 n: in_shape[0].unwrap_static() as u32,
4903 c_in: in_shape[1].unwrap_static() as u32,
4904 c_out: out_shape[1].unwrap_static() as u32,
4905 l: in_shape[2].unwrap_static() as u32,
4906 l_out: out_shape[2].unwrap_static() as u32,
4907 kl: kernel_size[0] as u32,
4908 sl: s(0),
4909 pl: p(0),
4910 dl: d(0),
4911 groups: *groups as u32,
4912 in_off,
4913 w_off,
4914 out_off,
4915 _p0: 0,
4916 _p1: 0,
4917 _p2: 0,
4918 };
4919 schedule.push(Step::Conv1d { params: p1 });
4920 let ck = conv1d_kernel(&dev.device);
4921 let u = emit_uniform(std::mem::size_of::<Conv1dParams>());
4922 let bg = bind_two_buf0_window(
4923 &dev.device,
4924 ck,
4925 &arena.buffer,
4926 base,
4927 size,
4928 &u,
4929 );
4930 uniforms.push(u);
4931 bind_groups.push(bg);
4932 }
4933 (2, 4, 4, 4) => {
4934 let h_in = in_shape[2].unwrap_static() as u32;
4935 let w_in = in_shape[3].unwrap_static() as u32;
4936 let one_d = h_in == 1
4943 && w_in > 1
4944 && kernel_size[0] > 1
4945 && kernel_size.get(1).copied().unwrap_or(1) == 1;
4946 let (h, w, h_out, w_out, kh, kw, sh, sw, ph, pw, dh, dw) = if one_d {
4947 (
4948 w_in,
4949 1,
4950 out_shape[3].unwrap_static() as u32,
4951 1,
4952 kernel_size[0] as u32,
4953 1,
4954 s(0),
4955 1,
4956 p(0),
4957 0,
4958 d(0),
4959 1,
4960 )
4961 } else {
4962 (
4963 h_in,
4964 w_in,
4965 out_shape[2].unwrap_static() as u32,
4966 out_shape[3].unwrap_static() as u32,
4967 kernel_size[0] as u32,
4968 kernel_size[1] as u32,
4969 s(0),
4970 s(1),
4971 p(0),
4972 p(1),
4973 d(0),
4974 d(1),
4975 )
4976 };
4977 let p2 = Conv2dParams {
4978 n: in_shape[0].unwrap_static() as u32,
4979 c_in: in_shape[1].unwrap_static() as u32,
4980 c_out: out_shape[1].unwrap_static() as u32,
4981 h,
4982 w,
4983 h_out,
4984 w_out,
4985 kh,
4986 kw,
4987 sh,
4988 sw,
4989 ph,
4990 pw,
4991 dh,
4992 dw,
4993 groups: *groups as u32,
4994 in_off,
4995 w_off,
4996 out_off,
4997 };
4998 schedule.push(Step::Conv2d { params: p2 });
4999 let ck = conv2d_kernel(&dev.device);
5000 let u = emit_uniform(std::mem::size_of::<Conv2dParams>());
5001 let bg = bind_two_buf0_window(
5002 &dev.device,
5003 ck,
5004 &arena.buffer,
5005 base,
5006 size,
5007 &u,
5008 );
5009 uniforms.push(u);
5010 bind_groups.push(bg);
5011 }
5012 (3, 5, 5, 5) => {
5013 let p3 = Conv3dParams {
5014 n: in_shape[0].unwrap_static() as u32,
5015 c_in: in_shape[1].unwrap_static() as u32,
5016 c_out: out_shape[1].unwrap_static() as u32,
5017 d: in_shape[2].unwrap_static() as u32,
5018 h: in_shape[3].unwrap_static() as u32,
5019 w: in_shape[4].unwrap_static() as u32,
5020 d_out: out_shape[2].unwrap_static() as u32,
5021 h_out: out_shape[3].unwrap_static() as u32,
5022 w_out: out_shape[4].unwrap_static() as u32,
5023 kd: kernel_size[0] as u32,
5024 kh: kernel_size[1] as u32,
5025 kw: kernel_size[2] as u32,
5026 sd: s(0),
5027 sh: s(1),
5028 sw: s(2),
5029 pd: p(0),
5030 ph: p(1),
5031 pw: p(2),
5032 dd: d(0),
5033 dh: d(1),
5034 dw: d(2),
5035 groups: *groups as u32,
5036 in_off,
5037 w_off,
5038 out_off,
5039 _p0: 0,
5040 };
5041 schedule.push(Step::Conv3d { params: p3 });
5042 let ck = conv3d_kernel(&dev.device);
5043 let u = emit_uniform(std::mem::size_of::<Conv3dParams>());
5044 let bg = bind_two_buf0_window(
5045 &dev.device,
5046 ck,
5047 &arena.buffer,
5048 base,
5049 size,
5050 &u,
5051 );
5052 uniforms.push(u);
5053 bind_groups.push(bg);
5054 }
5055 (k, ni, wi, mi) => panic!(
5056 "rlx-wgpu Conv: rank kernel={k} in={ni} weight={wi} out={mi} \
5057 not supported (use 1D/2D/3D NCHW)"
5058 ),
5059 }
5060 }
5061
5062 Op::Im2Col {
5063 kernel_size,
5064 stride,
5065 padding,
5066 dilation,
5067 } => {
5068 let x_shape = &graph.node(node.inputs[0]).shape;
5069 if kernel_size.len() != 2 || x_shape.rank() != 4 {
5070 panic!("rlx-wgpu Im2Col: 2D NCHW only");
5071 }
5072 let n = match x_shape.dim(0) {
5073 rlx_ir::shape::Dim::Static(v) => v as u32,
5074 _ => 0,
5075 };
5076 let c_in = x_shape.dim(1).unwrap_static() as u32;
5077 let h = x_shape.dim(2).unwrap_static() as u32;
5078 let w = x_shape.dim(3).unwrap_static() as u32;
5079 let kh = kernel_size[0] as u32;
5080 let kw = kernel_size[1] as u32;
5081 let sh = stride.first().copied().unwrap_or(1) as u32;
5082 let sw = stride.get(1).copied().unwrap_or(1) as u32;
5083 let ph = padding.first().copied().unwrap_or(0) as u32;
5084 let pw = padding.get(1).copied().unwrap_or(0) as u32;
5085 let dh = dilation.first().copied().unwrap_or(1) as u32;
5086 let dw_dil = dilation.get(1).copied().unwrap_or(1) as u32;
5087 let h_out = rlx_ir::shape::conv2d_spatial_output(
5088 h as usize,
5089 kh as usize,
5090 sh as usize,
5091 ph as usize,
5092 dh as usize,
5093 ) as u32;
5094 let w_out = rlx_ir::shape::conv2d_spatial_output(
5095 w as usize,
5096 kw as usize,
5097 sw as usize,
5098 pw as usize,
5099 dw_dil as usize,
5100 ) as u32;
5101 schedule.push(Step::Im2ColHost {
5102 x_byte_off: arena.offset(node.inputs[0]) as u32,
5103 col_byte_off: arena.offset(node.id) as u32,
5104 n,
5105 c_in,
5106 h,
5107 w,
5108 h_out,
5109 w_out,
5110 kh,
5111 kw,
5112 sh,
5113 sw,
5114 ph,
5115 pw,
5116 dh,
5117 dw_dil,
5118 });
5119 }
5120
5121 Op::Cumsum { axis, exclusive } => {
5122 let in_id = node.inputs[0];
5123 let in_shape = graph.node(in_id).shape.dims();
5124 let last = (in_shape.len() - 1) as i32;
5125 if *axis != -1 && *axis != last {
5126 panic!("rlx-wgpu Cumsum: only last-axis wired (got axis={axis})");
5127 }
5128 let inner = in_shape[in_shape.len() - 1].unwrap_static() as u32;
5129 let total: u32 = in_shape.iter().map(|d| d.unwrap_static() as u32).product();
5130 let outer = total / inner.max(1);
5131 let p = CumsumParams {
5132 outer,
5133 inner,
5134 in_off: (arena.offset(in_id) / 4) as u32,
5135 out_off: (arena.offset(node.id) / 4) as u32,
5136 exclusive: if *exclusive { 1 } else { 0 },
5137 _p0: 0,
5138 _p1: 0,
5139 _p2: 0,
5140 };
5141 schedule.push(Step::Cumsum { params: p });
5142 let ck2 = cumsum_kernel(&dev.device);
5143 let u = emit_uniform(std::mem::size_of::<CumsumParams>());
5144 let bg = bind_op_output_window(&dev.device, ck2, &arena, node.id, &u);
5145 uniforms.push(u);
5146 bind_groups.push(bg);
5147 }
5148 Op::Fft { inverse, norm } => {
5149 let in_id = node.inputs[0];
5150 let in_shape = graph.node(in_id).shape.clone();
5151 let meta = rlx_ir::fft::fft_meta(&in_shape);
5152 let dtype = in_shape.dtype();
5153 let use_gpu = rlx_ir::fft::gpu_fft_native_eligible(dtype, meta.n_complex)
5154 && meta.n_complex >= 2;
5155 let scale = norm.output_scale(meta.n_complex, *inverse) as f32;
5156 if use_gpu {
5157 schedule.push(Step::FftGpu {
5158 src_off: (arena.offset(in_id) / 4) as u32,
5159 dst_off: (arena.offset(node.id) / 4) as u32,
5160 outer: meta.outer as u32,
5161 n: meta.n_complex as u32,
5162 inverse: if *inverse { 1 } else { 0 },
5163 norm_scale: scale,
5164 });
5165 fft_gpu_steps.push(crate::fft_dispatch::FftGpuResources::new(
5166 &dev.device,
5167 &arena.buffer,
5168 ));
5169 } else {
5170 schedule.push(Step::FftHost {
5171 src_byte_off: arena.offset(in_id) as u32,
5172 dst_byte_off: arena.offset(node.id) as u32,
5173 outer: meta.outer as u32,
5174 n_complex: meta.n_complex as u32,
5175 inverse: *inverse,
5176 norm_tag: norm.tag(),
5177 dtype_tag: fft_dtype_tag(dtype),
5178 });
5179 }
5180 }
5181 Op::WelchPeaks { k, n_segments } => {
5182 let spec_shape = graph.node(node.inputs[0]).shape.clone();
5183 let meta = rlx_ir::audio::welch_peaks_meta(&spec_shape, *k, *n_segments)
5184 .unwrap_or_else(|e| panic!("Op::WelchPeaks: {e}"));
5185 let use_gpu = rlx_ir::audio::welch_peaks_gpu_native_eligible(
5186 &spec_shape,
5187 *k,
5188 *n_segments,
5189 )
5190 .unwrap_or(false);
5191 if use_gpu {
5192 let p = WelchPeaksGpuParams {
5193 spec_off: (arena.offset(node.inputs[0]) / 4) as u32,
5194 dst_off: (arena.offset(node.id) / 4) as u32,
5195 welch_batch: meta.welch_batch as u32,
5196 n_fft: meta.n_fft as u32,
5197 n_segments: meta.n_segments as u32,
5198 k: meta.k as u32,
5199 n_bins: meta.n_bins as u32,
5200 _p0: 0,
5201 _p1: 0,
5202 };
5203 schedule.push(Step::WelchPeaksGpu { params: p });
5204 let wk = welch_peaks_gpu_kernel(&dev.device);
5205 let u = emit_uniform(std::mem::size_of::<WelchPeaksGpuParams>());
5206 let bg = bind_op_output_window(&dev.device, wk, &arena, node.id, &u);
5207 uniforms.push(u);
5208 bind_groups.push(bg);
5209 } else {
5210 schedule.push(Step::WelchPeaksHost {
5211 spec_byte_off: arena.offset(node.inputs[0]) as u32,
5212 dst_byte_off: arena.offset(node.id) as u32,
5213 welch_batch: meta.welch_batch as u32,
5214 n_fft: meta.n_fft as u32,
5215 n_segments: meta.n_segments as u32,
5216 k: meta.k as u32,
5217 });
5218 }
5219 }
5220 Op::LogMel => {
5221 let spec_shape = graph.node(node.inputs[0]).shape.clone();
5222 let filt_shape = graph.node(node.inputs[1]).shape.clone();
5223 let meta = rlx_ir::audio::log_mel_meta(&spec_shape, &filt_shape)
5224 .unwrap_or_else(|e| panic!("Op::LogMel: {e}"));
5225 schedule.push(Step::LogMelHost {
5226 spec_byte_off: arena.offset(node.inputs[0]) as u32,
5227 filt_byte_off: arena.offset(node.inputs[1]) as u32,
5228 dst_byte_off: arena.offset(node.id) as u32,
5229 outer: meta.outer as u32,
5230 n_fft: meta.n_fft as u32,
5231 n_bins: meta.n_bins as u32,
5232 n_mels: meta.n_mels as u32,
5233 });
5234 }
5235 Op::LogMelBackward => {
5236 let spec_shape = graph.node(node.inputs[0]).shape.clone();
5237 let filt_shape = graph.node(node.inputs[1]).shape.clone();
5238 let meta = rlx_ir::audio::log_mel_meta(&spec_shape, &filt_shape)
5239 .unwrap_or_else(|e| panic!("Op::LogMelBackward: {e}"));
5240 schedule.push(Step::LogMelBackwardHost {
5241 spec_byte_off: arena.offset(node.inputs[0]) as u32,
5242 filt_byte_off: arena.offset(node.inputs[1]) as u32,
5243 dy_byte_off: arena.offset(node.inputs[2]) as u32,
5244 dst_byte_off: arena.offset(node.id) as u32,
5245 outer: meta.outer as u32,
5246 n_fft: meta.n_fft as u32,
5247 n_bins: meta.n_bins as u32,
5248 n_mels: meta.n_mels as u32,
5249 });
5250 }
5251 Op::SelectiveScan { state_size } => {
5252 if *state_size > 256 {
5253 panic!(
5254 "rlx-wgpu SelectiveScan: state_size {} exceeds compile-time \
5255 cap of 256 (kernel uses fixed-size private array)",
5256 state_size
5257 );
5258 }
5259 let x_id = node.inputs[0];
5260 let dt_id = node.inputs[1];
5261 let a_id = node.inputs[2];
5262 let b_id = node.inputs[3];
5263 let c_id = node.inputs[4];
5264 let in_dims = graph.node(x_id).shape.dims();
5265 let seq = in_dims[1].unwrap_static() as u32;
5266 let p = SelectiveScanParams {
5267 batch: in_dims[0].unwrap_static() as u32,
5268 seq,
5269 hidden: in_dims[2].unwrap_static() as u32,
5270 state_size: *state_size as u32,
5271 x_off: (arena.offset(x_id) / 4) as u32,
5272 delta_off: (arena.offset(dt_id) / 4) as u32,
5273 a_off: (arena.offset(a_id) / 4) as u32,
5274 b_off: (arena.offset(b_id) / 4) as u32,
5275 c_off: (arena.offset(c_id) / 4) as u32,
5276 out_off: (arena.offset(node.id) / 4) as u32,
5277 seq_stride: seq,
5280 _p1: 0,
5281 _p2: 0,
5282 _p3: 0,
5283 _p4: 0,
5284 _p5: 0,
5285 };
5286 schedule.push(Step::SelectiveScan { params: p });
5287 let ssk = selective_scan_kernel(&dev.device);
5288 let u = emit_uniform(std::mem::size_of::<SelectiveScanParams>());
5289 let bg = bind_op_output_window(&dev.device, ssk, &arena, node.id, &u);
5290 uniforms.push(u);
5291 bind_groups.push(bg);
5292 }
5293 Op::Mamba2 {
5294 head_dim,
5295 state_size,
5296 } => {
5297 if *state_size > 256 {
5298 panic!(
5299 "rlx-wgpu Mamba2: state_size {} exceeds compile-time cap of 256",
5300 state_size
5301 );
5302 }
5303 let x_id = node.inputs[0];
5304 let in_dims = graph.node(x_id).shape.dims(); let seq = in_dims[1].unwrap_static() as u32;
5306 let p = Mamba2Params {
5307 batch: in_dims[0].unwrap_static() as u32,
5308 seq,
5309 heads: in_dims[2].unwrap_static() as u32,
5310 head_dim: *head_dim as u32,
5311 state_size: *state_size as u32,
5312 x_off: (arena.offset(x_id) / 4) as u32,
5313 dt_off: (arena.offset(node.inputs[1]) / 4) as u32,
5314 a_off: (arena.offset(node.inputs[2]) / 4) as u32,
5315 b_off: (arena.offset(node.inputs[3]) / 4) as u32,
5316 c_off: (arena.offset(node.inputs[4]) / 4) as u32,
5317 out_off: (arena.offset(node.id) / 4) as u32,
5318 seq_stride: seq,
5319 _p1: 0,
5320 _p2: 0,
5321 _p3: 0,
5322 _p4: 0,
5323 };
5324 schedule.push(Step::Mamba2 { params: p });
5325 let mk = mamba2_kernel(&dev.device);
5326 let u = emit_uniform(std::mem::size_of::<Mamba2Params>());
5327 let bg = bind_op_output_window(&dev.device, mk, &arena, node.id, &u);
5328 uniforms.push(u);
5329 bind_groups.push(bg);
5330 }
5331 Op::Gru {
5332 hidden_size,
5333 num_layers,
5334 bidirectional,
5335 carry,
5336 } => {
5337 let x_id = node.inputs[0];
5338 let in_dims = graph.node(x_id).shape.dims(); let batch = in_dims[0].unwrap_static() as u32;
5340 let seq = in_dims[1].unwrap_static() as u32;
5341 let input_size = in_dims[2].unwrap_static() as u32;
5342 let hidden = *hidden_size as u32;
5343 let simple = *num_layers == 1 && !*bidirectional && !*carry;
5344 if simple && hidden <= 256 {
5345 let p = GruParams {
5346 batch,
5347 seq,
5348 input_size,
5349 hidden,
5350 x_off: (arena.offset(x_id) / 4) as u32,
5351 wih_off: (arena.offset(node.inputs[1]) / 4) as u32,
5352 whh_off: (arena.offset(node.inputs[2]) / 4) as u32,
5353 bih_off: (arena.offset(node.inputs[3]) / 4) as u32,
5354 bhh_off: (arena.offset(node.inputs[4]) / 4) as u32,
5355 out_off: (arena.offset(node.id) / 4) as u32,
5356 seq_stride: seq,
5357 _p1: 0,
5358 _p2: 0,
5359 _p3: 0,
5360 _p4: 0,
5361 _p5: 0,
5362 };
5363 schedule.push(Step::Gru { params: p });
5364 let gk = gru_kernel(&dev.device);
5365 let u = emit_uniform(std::mem::size_of::<GruParams>());
5366 let bg = bind_op_output_window(&dev.device, gk, &arena, node.id, &u);
5367 uniforms.push(u);
5368 bind_groups.push(bg);
5369 } else {
5370 let h0 = if *carry {
5371 arena.offset(node.inputs[5]) as u32
5372 } else {
5373 0
5374 };
5375 schedule.push(Step::GruHost {
5376 x: arena.offset(x_id) as u32,
5377 w_ih: arena.offset(node.inputs[1]) as u32,
5378 w_hh: arena.offset(node.inputs[2]) as u32,
5379 b_ih: arena.offset(node.inputs[3]) as u32,
5380 b_hh: arena.offset(node.inputs[4]) as u32,
5381 h0,
5382 dst: arena.offset(node.id) as u32,
5383 batch,
5384 seq,
5385 input_size,
5386 hidden,
5387 num_layers: *num_layers as u32,
5388 bidirectional: *bidirectional,
5389 carry: *carry,
5390 });
5391 }
5392 }
5393 Op::Rnn {
5394 hidden_size,
5395 num_layers,
5396 bidirectional,
5397 carry,
5398 relu,
5399 } => {
5400 let x_id = node.inputs[0];
5401 let in_dims = graph.node(x_id).shape.dims();
5402 let batch = in_dims[0].unwrap_static() as u32;
5403 let seq = in_dims[1].unwrap_static() as u32;
5404 let input_size = in_dims[2].unwrap_static() as u32;
5405 let hidden = *hidden_size as u32;
5406 let simple = *num_layers == 1 && !*bidirectional && !*carry;
5407 if simple && hidden <= 256 {
5408 let p = RnnParams {
5409 batch,
5410 seq,
5411 input_size,
5412 hidden,
5413 x_off: (arena.offset(x_id) / 4) as u32,
5414 wih_off: (arena.offset(node.inputs[1]) / 4) as u32,
5415 whh_off: (arena.offset(node.inputs[2]) / 4) as u32,
5416 bias_off: (arena.offset(node.inputs[3]) / 4) as u32,
5417 out_off: (arena.offset(node.id) / 4) as u32,
5418 seq_stride: seq,
5419 relu: u32::from(*relu),
5420 _p1: 0,
5421 _p2: 0,
5422 _p3: 0,
5423 _p4: 0,
5424 _p5: 0,
5425 };
5426 schedule.push(Step::Rnn { params: p });
5427 let rk = rnn_kernel(&dev.device);
5428 let u = emit_uniform(std::mem::size_of::<RnnParams>());
5429 let bg = bind_op_output_window(&dev.device, rk, &arena, node.id, &u);
5430 uniforms.push(u);
5431 bind_groups.push(bg);
5432 } else {
5433 let h0 = if *carry {
5434 arena.offset(node.inputs[4]) as u32
5435 } else {
5436 0
5437 };
5438 schedule.push(Step::RnnHost {
5439 x: arena.offset(x_id) as u32,
5440 w_ih: arena.offset(node.inputs[1]) as u32,
5441 w_hh: arena.offset(node.inputs[2]) as u32,
5442 bias: arena.offset(node.inputs[3]) as u32,
5443 h0,
5444 dst: arena.offset(node.id) as u32,
5445 batch,
5446 seq,
5447 input_size,
5448 hidden,
5449 num_layers: *num_layers as u32,
5450 bidirectional: *bidirectional,
5451 carry: *carry,
5452 relu: *relu,
5453 });
5454 }
5455 }
5456 Op::GatedDeltaNet {
5457 state_size,
5458 carry_state,
5459 } => {
5460 if *state_size > rlx_cpu::gdn::GDN_MAX_STATE {
5461 panic!(
5462 "rlx-wgpu GatedDeltaNet: state_size {state_size} > {}",
5463 rlx_cpu::gdn::GDN_MAX_STATE
5464 );
5465 }
5466 let q_id = node.inputs[0];
5467 let q_shape = &graph.node(q_id).shape;
5468 let state_off = if *carry_state {
5469 arena.offset(node.inputs[5])
5470 } else {
5471 0
5472 };
5473 schedule.push(Step::GatedDeltaNet {
5474 q_byte_off: arena.offset(q_id) as u32,
5475 k_byte_off: arena.offset(node.inputs[1]) as u32,
5476 v_byte_off: arena.offset(node.inputs[2]) as u32,
5477 g_byte_off: arena.offset(node.inputs[3]) as u32,
5478 beta_byte_off: arena.offset(node.inputs[4]) as u32,
5479 state_byte_off: state_off as u32,
5480 dst_byte_off: arena.offset(node.id) as u32,
5481 batch: q_shape.dim(0).unwrap_static() as u32,
5482 seq: q_shape.dim(1).unwrap_static() as u32,
5483 heads: q_shape.dim(2).unwrap_static() as u32,
5484 state_size: *state_size as u32,
5485 use_carry: *carry_state,
5486 });
5487 if gguf_host_pad.is_none() {
5488 let bk = binary_kernel(&dev.device);
5489 let u = emit_uniform(256);
5490 gguf_host_pad = Some((
5491 u.clone(),
5492 bind_op_output_window(&dev.device, bk, &arena, node.id, &u),
5493 ));
5494 }
5495 let (u, bg) = gguf_host_pad.as_ref().unwrap();
5496 uniforms.push(u.clone());
5497 bind_groups.push(bg.clone());
5498 }
5499 Op::Lstm {
5500 hidden_size,
5501 num_layers,
5502 bidirectional,
5503 carry,
5504 } => {
5505 let x_shape = &graph.node(node.inputs[0]).shape;
5506 let (h0, c0) = if *carry {
5507 (
5508 arena.offset(node.inputs[4]) as u32,
5509 arena.offset(node.inputs[5]) as u32,
5510 )
5511 } else {
5512 (0u32, 0u32)
5513 };
5514 schedule.push(Step::Lstm {
5515 x_byte_off: arena.offset(node.inputs[0]) as u32,
5516 w_ih_byte_off: arena.offset(node.inputs[1]) as u32,
5517 w_hh_byte_off: arena.offset(node.inputs[2]) as u32,
5518 bias_byte_off: arena.offset(node.inputs[3]) as u32,
5519 h0_byte_off: h0,
5520 c0_byte_off: c0,
5521 dst_byte_off: arena.offset(node.id) as u32,
5522 batch: x_shape.dim(0).unwrap_static() as u32,
5523 seq: x_shape.dim(1).unwrap_static() as u32,
5524 input_size: x_shape.dim(2).unwrap_static() as u32,
5525 hidden: *hidden_size as u32,
5526 num_layers: *num_layers as u32,
5527 bidirectional: *bidirectional,
5528 carry: *carry,
5529 });
5530 if gguf_host_pad.is_none() {
5532 let bk = binary_kernel(&dev.device);
5533 let u = emit_uniform(256);
5534 gguf_host_pad = Some((
5535 u.clone(),
5536 bind_op_output_window(&dev.device, bk, &arena, node.id, &u),
5537 ));
5538 }
5539 let (u, bg) = gguf_host_pad.as_ref().unwrap();
5540 uniforms.push(u.clone());
5541 bind_groups.push(bg.clone());
5542 }
5543 Op::ConvTranspose2d {
5544 kernel_size,
5545 stride,
5546 padding,
5547 dilation,
5548 output_padding: _,
5549 groups,
5550 } => {
5551 let in_shape = &graph.node(node.inputs[0]).shape;
5552 let out_shape = &node.shape;
5553 schedule.push(Step::ConvTranspose2d {
5554 src_byte_off: arena.offset(node.inputs[0]) as u32,
5555 weight_byte_off: arena.offset(node.inputs[1]) as u32,
5556 dst_byte_off: arena.offset(node.id) as u32,
5557 n: in_shape.dim(0).unwrap_static() as u32,
5558 c_in: in_shape.dim(1).unwrap_static() as u32,
5559 h: in_shape.dim(2).unwrap_static() as u32,
5560 w_in: in_shape.dim(3).unwrap_static() as u32,
5561 c_out: out_shape.dim(1).unwrap_static() as u32,
5562 h_out: out_shape.dim(2).unwrap_static() as u32,
5563 w_out: out_shape.dim(3).unwrap_static() as u32,
5564 kh: kernel_size[0] as u32,
5565 kw: kernel_size[1] as u32,
5566 sh: stride[0] as u32,
5567 sw: stride[1] as u32,
5568 ph: padding[0] as u32,
5569 pw: padding[1] as u32,
5570 dh: dilation[0] as u32,
5571 dw: dilation[1] as u32,
5572 groups: *groups as u32,
5573 });
5574 }
5578 Op::GroupNorm { num_groups, eps } => {
5579 let in_shape = &graph.node(node.inputs[0]).shape;
5581 schedule.push(Step::GroupNormHost {
5582 src_byte_off: arena.offset(node.inputs[0]) as u32,
5583 gamma_byte_off: arena.offset(node.inputs[1]) as u32,
5584 beta_byte_off: arena.offset(node.inputs[2]) as u32,
5585 dst_byte_off: arena.offset(node.id) as u32,
5586 n: in_shape.dim(0).unwrap_static() as u32,
5587 c: in_shape.dim(1).unwrap_static() as u32,
5588 h: in_shape.dim(2).unwrap_static() as u32,
5589 w: in_shape.dim(3).unwrap_static() as u32,
5590 num_groups: *num_groups as u32,
5591 eps: *eps,
5592 });
5593 }
5594 Op::LayerNorm2d { eps } => {
5595 let in_shape = &graph.node(node.inputs[0]).shape;
5597 schedule.push(Step::LayerNorm2dHost {
5598 src_byte_off: arena.offset(node.inputs[0]) as u32,
5599 gamma_byte_off: arena.offset(node.inputs[1]) as u32,
5600 beta_byte_off: arena.offset(node.inputs[2]) as u32,
5601 dst_byte_off: arena.offset(node.id) as u32,
5602 n: in_shape.dim(0).unwrap_static() as u32,
5603 c: in_shape.dim(1).unwrap_static() as u32,
5604 h: in_shape.dim(2).unwrap_static() as u32,
5605 w: in_shape.dim(3).unwrap_static() as u32,
5606 eps: *eps,
5607 });
5608 }
5609 Op::ResizeNearest2x => {
5610 let in_shape = &graph.node(node.inputs[0]).shape;
5612 schedule.push(Step::ResizeNearest2xHost {
5613 src_byte_off: arena.offset(node.inputs[0]) as u32,
5614 dst_byte_off: arena.offset(node.id) as u32,
5615 n: in_shape.dim(0).unwrap_static() as u32,
5616 c: in_shape.dim(1).unwrap_static() as u32,
5617 h: in_shape.dim(2).unwrap_static() as u32,
5618 w: in_shape.dim(3).unwrap_static() as u32,
5619 });
5620 }
5621 Op::Reverse { axes } => {
5622 let in_shape = &graph.node(node.inputs[0]).shape;
5624 let rank = in_shape.rank();
5625 let dims: Vec<u32> = (0..rank)
5626 .map(|i| in_shape.dim(i).unwrap_static() as u32)
5627 .collect();
5628 let mut rev_mask = vec![false; rank];
5629 for &a in axes {
5630 if a < rank {
5631 rev_mask[a] = true;
5632 }
5633 }
5634 schedule.push(Step::ReverseHost {
5635 src_byte_off: arena.offset(node.inputs[0]) as u32,
5636 dst_byte_off: arena.offset(node.id) as u32,
5637 dims,
5638 rev_mask,
5639 elem_bytes: in_shape.dtype().size_bytes() as u32,
5640 });
5641 }
5642 Op::ArgMax { axis, keep_dim: _ } | Op::ArgMin { axis, keep_dim: _ } => {
5643 let in_shape = &graph.node(node.inputs[0]).shape;
5645 let rank = in_shape.rank();
5646 let outer: usize = (0..*axis)
5647 .map(|i| in_shape.dim(i).unwrap_static())
5648 .product::<usize>()
5649 .max(1);
5650 let reduced = in_shape.dim(*axis).unwrap_static();
5651 let inner: usize = (*axis + 1..rank)
5652 .map(|i| in_shape.dim(i).unwrap_static())
5653 .product::<usize>()
5654 .max(1);
5655 schedule.push(Step::ArgReduceHost {
5656 src_byte_off: arena.offset(node.inputs[0]) as u32,
5657 dst_byte_off: arena.offset(node.id) as u32,
5658 outer: outer as u32,
5659 reduced: reduced as u32,
5660 inner: inner as u32,
5661 is_max: matches!(node.op, Op::ArgMax { .. }),
5662 });
5663 }
5664 Op::Custom { name, attrs, .. } => match name.as_str() {
5665 "llada2.group_limited_gate" => {
5666 let sig_id = node.inputs[0];
5667 let route_id = node.inputs[1];
5668 let n_elems = graph.node(sig_id).shape.num_elements().unwrap() as u32;
5669 let mut attr_buf = [0u8; 20];
5670 let n = attrs.len().min(20);
5671 attr_buf[..n].copy_from_slice(&attrs[..n]);
5672 schedule.push(Step::Llada2GroupLimitedGate {
5673 sig_byte_off: arena.offset(sig_id) as u32,
5674 route_byte_off: arena.offset(route_id) as u32,
5675 out_byte_off: arena.offset(node.id) as u32,
5676 n_elems,
5677 attrs: attr_buf,
5678 });
5679 }
5680 "umap.knn" => {
5681 let pw_id = node.inputs[0];
5682 let pw_shape = graph.node(pw_id).shape.dims();
5683 let n = pw_shape[0].unwrap_static() as u32;
5684 let k = if attrs.len() >= 4 {
5685 u32::from_le_bytes(attrs[..4].try_into().unwrap())
5686 } else {
5687 panic!("rlx-wgpu: umap.knn attrs missing k");
5688 };
5689 let pw_off = arena.offset(pw_id) as u32;
5690 let out_off = arena.offset(node.id) as u32;
5691 if n as usize >= crate::umap_knn_host::UMAP_KNN_GPU_MIN_N {
5692 let p = UmapKnnParams {
5693 n,
5694 k,
5695 pw_off: pw_off / 4,
5696 out_off: out_off / 4,
5697 _p0: 0,
5698 _p1: 0,
5699 _p2: 0,
5700 };
5701 schedule.push(Step::UmapKnn { params: p });
5702 let uk = umap_knn_kernel(&dev.device);
5703 let u = emit_uniform(std::mem::size_of::<UmapKnnParams>());
5704 let bg = bind_op_output_window(&dev.device, uk, &arena, node.id, &u);
5705 uniforms.push(u);
5706 bind_groups.push(bg);
5707 } else {
5708 schedule.push(Step::UmapKnnHost {
5709 pairwise_byte_off: pw_off,
5710 out_byte_off: out_off,
5711 n,
5712 k,
5713 });
5714 }
5715 }
5716 "gdino.ms_deform_attn" => {
5717 let in_offs: Vec<(u32, u32)> = node
5718 .inputs
5719 .iter()
5720 .map(|&id| {
5721 let bytes = graph.node(id).shape.num_elements().unwrap() * 4;
5722 (arena.offset(id) as u32, bytes as u32)
5723 })
5724 .collect();
5725 let out_bytes = (node.shape.num_elements().unwrap() * 4) as u32;
5726 schedule.push(Step::MsDeformAttnHost {
5727 in_offs,
5728 out_byte_off: arena.offset(node.id) as u32,
5729 out_bytes,
5730 attrs: attrs.clone(),
5731 });
5732 }
5733 other => panic!("rlx-wgpu: unsupported Op::Custom('{other}')"),
5734 },
5735 Op::GroupedMatMul => {
5736 let in_id = node.inputs[0];
5738 let w_id = node.inputs[1];
5739 let idx_id = node.inputs[2];
5740 let in_dims = graph.node(in_id).shape.dims();
5741 let w_dims = graph.node(w_id).shape.dims();
5742 let m = in_dims[0].unwrap_static() as u32;
5743 let k = in_dims[1].unwrap_static() as u32;
5744 let n = w_dims[2].unwrap_static() as u32;
5745 let ne = w_dims[0].unwrap_static() as u32;
5746 let p = GroupedMatmulParams {
5747 m,
5748 k,
5749 n,
5750 num_experts: ne,
5751 in_off: (arena.offset(in_id) / 4) as u32,
5752 w_off: (arena.offset(w_id) / 4) as u32,
5753 idx_off: (arena.offset(idx_id) / 4) as u32,
5754 out_off: (arena.offset(node.id) / 4) as u32,
5755 };
5756 schedule.push(Step::GroupedMatmul { params: p });
5757 let gk = grouped_matmul_kernel(&dev.device);
5758 let u = emit_uniform(std::mem::size_of::<GroupedMatmulParams>());
5759 let bg = bind_op_output_window(&dev.device, gk, &arena, node.id, &u);
5760 uniforms.push(u);
5761 bind_groups.push(bg);
5762 }
5763 Op::DequantGroupedMatMul { scheme } => {
5764 let in_id = node.inputs[0];
5765 let w_id = node.inputs[1];
5766 let idx_id = node.inputs[2];
5767 let in_dims = graph.node(in_id).shape.dims();
5768 let out_dims = node.shape.dims();
5769 let m = in_dims[0].unwrap_static() as u32;
5770 let k = in_dims[1].unwrap_static() as u32;
5771 let n = out_dims[out_dims.len() - 1].unwrap_static() as u32;
5772 let block_elems = scheme.gguf_block_size() as usize;
5773 let block_bytes = scheme.gguf_block_bytes() as usize;
5774 let slab_bytes = (k as usize * n as usize) / block_elems * block_bytes;
5775 let total_bytes = graph.node(w_id).shape.num_elements().unwrap();
5776 let ne = (total_bytes / slab_bytes.max(1)) as u32;
5777 schedule.push(Step::DequantGroupedMatmulGguf {
5778 m,
5779 k,
5780 n,
5781 num_experts: ne,
5782 scheme_id: crate::gguf_host::gguf_scheme_id(*scheme),
5783 x_byte_off: arena.offset(in_id) as u64,
5784 w_byte_off: arena.offset(w_id) as u64,
5785 idx_byte_off: arena.offset(idx_id) as u64,
5786 out_byte_off: arena.offset(node.id) as u64,
5787 });
5788 }
5793 Op::TopK { k } => {
5794 let in_id = node.inputs[0];
5795 let in_dims = graph.node(in_id).shape.dims();
5796 let inner = in_dims.last().unwrap().unwrap_static() as u32;
5797 let outer: u32 = in_dims[..in_dims.len() - 1]
5798 .iter()
5799 .map(|d| d.unwrap_static() as u32)
5800 .product::<u32>()
5801 .max(1);
5802 let p = TopKParams {
5803 outer,
5804 inner,
5805 k: *k as u32,
5806 in_off: (arena.offset(in_id) / 4) as u32,
5807 out_off: (arena.offset(node.id) / 4) as u32,
5808 _p0: 0,
5809 _p1: 0,
5810 _p2: 0,
5811 };
5812 schedule.push(Step::TopK { params: p });
5813 let tk = topk_kernel(&dev.device);
5814 let u = emit_uniform(std::mem::size_of::<TopKParams>());
5815 let bg = bind_op_output_window(&dev.device, tk, &arena, node.id, &u);
5816 uniforms.push(u);
5817 bind_groups.push(bg);
5818 }
5819 Op::ScatterAdd => {
5820 let upd_id = node.inputs[0];
5825 let idx_id = node.inputs[1];
5826 let upd_dims = graph.node(upd_id).shape.dims();
5827 let out_dims = node.shape.dims();
5828 let num_updates = upd_dims[0].unwrap_static() as u32;
5829 let trailing: u32 = upd_dims
5830 .iter()
5831 .skip(1)
5832 .map(|d| d.unwrap_static() as u32)
5833 .product::<u32>()
5834 .max(1);
5835 let out_dim = out_dims[0].unwrap_static() as u32;
5836 let out_total = out_dim * trailing;
5837
5838 let common = ScatterAddParams {
5839 op: 0,
5840 out_off: (arena.offset(node.id) / 4) as u32,
5841 upd_off: (arena.offset(upd_id) / 4) as u32,
5842 idx_off: (arena.offset(idx_id) / 4) as u32,
5843 out_total,
5844 num_updates,
5845 trailing,
5846 out_dim,
5847 };
5848 let sk = scatter_add_kernel(&dev.device);
5849
5850 schedule.push(Step::ScatterAdd { params: common });
5852 let u0 = emit_uniform(std::mem::size_of::<ScatterAddParams>());
5853 let bg0 = bind_op_output_window(&dev.device, sk, &arena, node.id, &u0);
5854 uniforms.push(u0);
5855 bind_groups.push(bg0);
5856
5857 let mut acc = common;
5859 acc.op = 1;
5860 schedule.push(Step::ScatterAdd { params: acc });
5861 let u1 = emit_uniform(std::mem::size_of::<ScatterAddParams>());
5862 let bg1 = bind_op_output_window(&dev.device, sk, &arena, node.id, &u1);
5863 uniforms.push(u1);
5864 bind_groups.push(bg1);
5865 }
5866 Op::FusedResidualLN { has_bias, eps } => {
5867 let x_id = node.inputs[0];
5869 let r_id = node.inputs[1];
5870 let (bias_id, g_id, b_id) = if *has_bias {
5871 (node.inputs[2], node.inputs[3], node.inputs[4])
5872 } else {
5873 (x_id, node.inputs[2], node.inputs[3]) };
5875 let in_dims = node.shape.dims();
5876 let inner = in_dims[in_dims.len() - 1].unwrap_static() as u32;
5877 let total: u32 = in_dims.iter().map(|d| d.unwrap_static() as u32).product();
5878 let outer = total / inner.max(1);
5879 let p = FusedResidualLnParams {
5880 outer,
5881 inner,
5882 in_off: (arena.offset(x_id) / 4) as u32,
5883 residual_off: (arena.offset(r_id) / 4) as u32,
5884 bias_off: (arena.offset(bias_id) / 4) as u32,
5885 gamma_off: (arena.offset(g_id) / 4) as u32,
5886 beta_off: (arena.offset(b_id) / 4) as u32,
5887 out_off: (arena.offset(node.id) / 4) as u32,
5888 eps_bits: eps.to_bits(),
5889 has_bias: if *has_bias { 1 } else { 0 },
5890 _p0: 0,
5891 _p1: 0,
5892 };
5893 schedule.push(Step::FusedResidualLn { params: p });
5894 let frk = fused_residual_ln_kernel(&dev.device);
5895 let u = emit_uniform(std::mem::size_of::<FusedResidualLnParams>());
5896 let bg = bind_op_output_window(&dev.device, frk, &arena, node.id, &u);
5897 uniforms.push(u);
5898 bind_groups.push(bg);
5899 }
5900 Op::FusedResidualRmsNorm { has_bias, eps } => {
5901 let x_id = node.inputs[0];
5902 let r_id = node.inputs[1];
5903 let (bias_id, g_id, b_id) = if *has_bias {
5904 (node.inputs[2], node.inputs[3], node.inputs[4])
5905 } else {
5906 (x_id, node.inputs[2], node.inputs[3])
5907 };
5908 let in_dims = node.shape.dims();
5909 let inner = in_dims[in_dims.len() - 1].unwrap_static() as u32;
5910 let total: u32 = in_dims.iter().map(|d| d.unwrap_static() as u32).product();
5911 let outer = total / inner.max(1);
5912 let p = FusedResidualRmsNormParams {
5913 outer,
5914 inner,
5915 in_off: (arena.offset(x_id) / 4) as u32,
5916 residual_off: (arena.offset(r_id) / 4) as u32,
5917 bias_off: (arena.offset(bias_id) / 4) as u32,
5918 gamma_off: (arena.offset(g_id) / 4) as u32,
5919 beta_off: (arena.offset(b_id) / 4) as u32,
5920 out_off: (arena.offset(node.id) / 4) as u32,
5921 eps_bits: eps.to_bits(),
5922 has_bias: if *has_bias { 1 } else { 0 },
5923 _p0: 0,
5924 _p1: 0,
5925 };
5926 schedule.push(Step::FusedResidualRmsNorm { params: p });
5927 let frk = fused_residual_rms_norm_kernel(&dev.device);
5928 let u = emit_uniform(std::mem::size_of::<FusedResidualRmsNormParams>());
5929 let bg = bind_op_output_window(&dev.device, frk, &arena, node.id, &u);
5930 uniforms.push(u);
5931 bind_groups.push(bg);
5932 }
5933 Op::DequantMatMul { scheme } => {
5934 use rlx_ir::QuantScheme;
5935 let x_id = node.inputs[0];
5936 let w_id = node.inputs[1];
5937 let out_total = node.shape.num_elements().unwrap_or(0) as u32;
5944 let n = node.shape.dim(node.shape.rank() - 1).unwrap_static() as u32;
5945 let m = out_total / n.max(1);
5946 let x_total = graph.node(x_id).shape.num_elements().unwrap_or(0) as u32;
5947 let k = x_total / m.max(1);
5948 if scheme.is_gguf() {
5949 schedule.push(Step::DequantMatmulGguf {
5950 m,
5951 k,
5952 n,
5953 scheme_id: crate::gguf_host::gguf_scheme_id(*scheme),
5954 x_byte_off: arena.offset(x_id) as u64,
5955 w_byte_off: arena.offset(w_id) as u64,
5956 out_byte_off: arena.offset(node.id) as u64,
5957 });
5958 } else {
5966 let (block_size, scheme_id) = match scheme {
5967 QuantScheme::Int8Block { block_size } => (*block_size, 0u32),
5968 QuantScheme::Int8BlockAsym { block_size } => (*block_size, 1u32),
5969 QuantScheme::Int4Block { block_size } => (*block_size, 2u32),
5970 QuantScheme::Fp8E4m3 => (1, 3u32),
5971 QuantScheme::Fp8E5m2 => (1, 4u32),
5972 QuantScheme::Nvfp4Block => (rlx_ir::NVFP4_GROUP_SIZE as u32, 5u32),
5973 other => panic!("rlx-wgpu DequantMatMul: unsupported scheme {other:?}"),
5974 };
5975 let scale_id = node.inputs[2];
5976 let zp_id = node.inputs[3];
5977 let p = DequantMatmulParams {
5978 m,
5979 k,
5980 n,
5981 block_size,
5982 scheme_id,
5983 x_off: (arena.offset(x_id) / 4) as u32,
5984 w_off: (arena.offset(w_id) / 4) as u32,
5985 scale_off: (arena.offset(scale_id) / 4) as u32,
5986 zp_off: (arena.offset(zp_id) / 4) as u32,
5987 out_off: (arena.offset(node.id) / 4) as u32,
5988 _p0: 0,
5989 _p1: 0,
5990 };
5991 schedule.push(Step::DequantMatmul { params: p });
5992 let dk = dequant_matmul_kernel(&dev.device);
5993 let u = emit_uniform(std::mem::size_of::<DequantMatmulParams>());
5994 let bg = bind_op_output_window(&dev.device, dk, &arena, node.id, &u);
5995 uniforms.push(u);
5996 bind_groups.push(bg);
5997 }
5998 }
5999 Op::RmsNormBackwardInput { eps, .. }
6000 | Op::RmsNormBackwardGamma { eps, .. }
6001 | Op::RmsNormBackwardBeta { eps, .. } => {
6002 let x_shape = &graph.node(node.inputs[0]).shape;
6003 let h = x_shape.dim(x_shape.rank() - 1).unwrap_static() as u32;
6004 let rows = (x_shape.num_elements().unwrap() / h.max(1) as usize) as u32;
6005 let foff = |i: usize| (arena.offset(node.inputs[i]) / 4) as u32;
6006 let wrt = match &node.op {
6007 Op::RmsNormBackwardInput { .. } => 0u32,
6008 Op::RmsNormBackwardGamma { .. } => 1u32,
6009 Op::RmsNormBackwardBeta { .. } => 2u32,
6010 _ => unreachable!(),
6011 };
6012 let p = RmsNormBwdParams {
6013 outer: rows,
6014 inner: h,
6015 x_off: foff(0),
6016 gamma_off: foff(1),
6017 beta_off: foff(2),
6018 dy_off: foff(3),
6019 out_off: (arena.offset(node.id) / 4) as u32,
6020 eps_bits: eps.to_bits(),
6021 wrt,
6022 };
6023 let rk = if wrt == 0 {
6024 rms_norm_backward_kernel(&dev.device)
6025 } else {
6026 rms_norm_backward_param_kernel(&dev.device)
6027 };
6028 let u = emit_uniform(std::mem::size_of::<RmsNormBwdParams>());
6029 let bg = bind_op_output_window(&dev.device, rk, &arena, node.id, &u);
6030 match &node.op {
6031 Op::RmsNormBackwardInput { .. } => {
6032 schedule.push(Step::RmsNormBackwardInput { params: p });
6033 }
6034 Op::RmsNormBackwardGamma { .. } => {
6035 schedule.push(Step::RmsNormBackwardGamma { params: p });
6036 }
6037 Op::RmsNormBackwardBeta { .. } => {
6038 schedule.push(Step::RmsNormBackwardBeta { params: p });
6039 }
6040 _ => unreachable!(),
6041 }
6042 uniforms.push(u);
6043 bind_groups.push(bg);
6044 }
6045 Op::LayerNormBackwardInput { eps, .. } => {
6046 let x_shape = &graph.node(node.inputs[0]).shape;
6047 let h = x_shape.dim(x_shape.rank() - 1).unwrap_static() as u32;
6048 let rows = (x_shape.num_elements().unwrap() / h.max(1) as usize) as u32;
6049 let p = LayerNormBwdParams {
6050 outer: rows,
6051 inner: h,
6052 x_off: (arena.offset(node.inputs[0]) / 4) as u32,
6053 gamma_off: (arena.offset(node.inputs[1]) / 4) as u32,
6054 dy_off: (arena.offset(node.inputs[2]) / 4) as u32,
6055 out_off: (arena.offset(node.id) / 4) as u32,
6056 eps_bits: eps.to_bits(),
6057 scratch_off: 0,
6058 };
6059 let rk = layer_norm_backward_input_kernel(&dev.device);
6060 let u = emit_uniform(std::mem::size_of::<LayerNormBwdParams>());
6061 let bg = bind_op_output_window(&dev.device, rk, &arena, node.id, &u);
6062 schedule.push(Step::LayerNormBackwardInput { params: p });
6063 uniforms.push(u);
6064 bind_groups.push(bg);
6065 }
6066 Op::LayerNormBackwardGamma { eps, .. } => {
6067 let x_shape = &graph.node(node.inputs[0]).shape;
6073 let h = x_shape.dim(x_shape.rank() - 1).unwrap_static() as u32;
6074 let rows = (x_shape.num_elements().unwrap() / h.max(1) as usize) as u32;
6075 const ROWS_PER_WG: u32 = 16;
6076 let num_workgroups = rows.div_ceil(ROWS_PER_WG.max(1));
6077 let scratch_off_words = (arena.scratch_off / 4) as u32;
6078 let partial_params = LayerNormBwdParams {
6079 outer: rows,
6080 inner: h,
6081 x_off: (arena.offset(node.inputs[0]) / 4) as u32,
6082 gamma_off: 0,
6083 dy_off: (arena.offset(node.inputs[1]) / 4) as u32,
6084 out_off: 0, eps_bits: eps.to_bits(),
6086 scratch_off: scratch_off_words,
6087 };
6088 let reduce_params = LayerNormBwdParams {
6089 outer: num_workgroups,
6092 inner: h,
6093 x_off: 0,
6094 gamma_off: 0,
6095 dy_off: 0,
6096 out_off: (arena.offset(node.id) / 4) as u32,
6097 eps_bits: eps.to_bits(),
6098 scratch_off: scratch_off_words,
6099 };
6100 let p_k = layer_norm_backward_gamma_partial_kernel(&dev.device);
6101 let r_k = layer_norm_backward_gamma_reduce_kernel(&dev.device);
6102 let p_u = emit_uniform(std::mem::size_of::<LayerNormBwdParams>());
6103 let r_u = emit_uniform(std::mem::size_of::<LayerNormBwdParams>());
6104 let p_bg = bind_op_output_window(&dev.device, p_k, &arena, node.id, &p_u);
6105 let r_bg = bind_op_output_window(&dev.device, r_k, &arena, node.id, &r_u);
6106 schedule.push(Step::LayerNormBackwardGammaPartial {
6107 params: partial_params,
6108 num_workgroups,
6109 });
6110 schedule.push(Step::LayerNormBackwardGammaReduce {
6111 params: reduce_params,
6112 });
6113 uniforms.push(p_u);
6114 uniforms.push(r_u);
6115 bind_groups.push(p_bg);
6116 bind_groups.push(r_bg);
6117 }
6118 Op::RopeBackward { head_dim, n_rot } => {
6119 let dy_shape = &graph.node(node.inputs[0]).shape;
6120 let (batch, seq, hidden) = if dy_shape.rank() >= 3 {
6121 (
6122 dy_shape.dim(0).unwrap_static() as u32,
6123 dy_shape.dim(1).unwrap_static() as u32,
6124 dy_shape.dim(2).unwrap_static() as u32,
6125 )
6126 } else {
6127 (
6128 1,
6129 dy_shape.dim(0).unwrap_static() as u32,
6130 dy_shape.dim(1).unwrap_static() as u32,
6131 )
6132 };
6133 let cos_len = graph.node(node.inputs[1]).shape.num_elements().unwrap() as u32;
6134 let p = RopeBwdParams {
6135 batch,
6136 seq,
6137 hidden,
6138 head_dim: *head_dim as u32,
6139 n_rot: *n_rot as u32,
6140 dy_off: (arena.offset(node.inputs[0]) / 4) as u32,
6141 cos_off: (arena.offset(node.inputs[1]) / 4) as u32,
6142 sin_off: (arena.offset(node.inputs[2]) / 4) as u32,
6143 dx_off: (arena.offset(node.id) / 4) as u32,
6144 cos_len,
6145 };
6146 let rk = rope_backward_kernel(&dev.device);
6147 let u = emit_uniform(std::mem::size_of::<RopeBwdParams>());
6148 let bg = bind_op_output_window(&dev.device, rk, &arena, node.id, &u);
6149 schedule.push(Step::RopeBackward { params: p });
6150 uniforms.push(u);
6151 bind_groups.push(bg);
6152 }
6153 Op::CumsumBackward { exclusive, .. } => {
6154 let dy_shape = &graph.node(node.inputs[0]).shape;
6155 let cols = dy_shape.dim(dy_shape.rank() - 1).unwrap_static() as u32;
6156 let rows = (dy_shape.num_elements().unwrap() / cols.max(1) as usize) as u32;
6157 let p = CumsumBwdParams {
6158 outer: rows,
6159 inner: cols,
6160 dy_off: (arena.offset(node.inputs[0]) / 4) as u32,
6161 dx_off: (arena.offset(node.id) / 4) as u32,
6162 exclusive: if *exclusive { 1 } else { 0 },
6163 _p0: 0,
6164 _p1: 0,
6165 _p2: 0,
6166 };
6167 let ck = cumsum_backward_kernel(&dev.device);
6168 let u = emit_uniform(std::mem::size_of::<CumsumBwdParams>());
6169 let bg = bind_op_output_window(&dev.device, ck, &arena, node.id, &u);
6170 schedule.push(Step::CumsumBackward { params: p });
6171 uniforms.push(u);
6172 bind_groups.push(bg);
6173 }
6174 Op::GatherBackward { .. } => {
6175 let dy_shape = &graph.node(node.inputs[0]).shape;
6176 let idx_shape = &graph.node(node.inputs[1]).shape;
6177 let out_shape = &node.shape;
6178 let rank = out_shape.rank();
6179 let axis = match &node.op {
6180 Op::GatherBackward { axis } => *axis,
6181 _ => 0,
6182 };
6183 let axis_u = if axis < 0 {
6184 (rank as i32 + axis) as usize
6185 } else {
6186 axis as usize
6187 };
6188 let outer: usize = (0..axis_u)
6189 .map(|i| dy_shape.dim(i).unwrap_static())
6190 .product::<usize>()
6191 .max(1);
6192 let num_idx = idx_shape.dim(axis_u).unwrap_static();
6193 let trailing: usize = (axis_u + 1..dy_shape.rank())
6194 .map(|i| dy_shape.dim(i).unwrap_static())
6195 .product::<usize>()
6196 .max(1);
6197 let axis_dim = out_shape.dim(axis_u).unwrap_static();
6198 let p = GatherBwdParams {
6199 outer: outer as u32,
6200 axis_dim: axis_dim as u32,
6201 num_idx: num_idx as u32,
6202 trailing: trailing as u32,
6203 dy_off: (arena.offset(node.inputs[0]) / 4) as u32,
6204 idx_off: (arena.offset(node.inputs[1]) / 4) as u32,
6205 dst_off: (arena.offset(node.id) / 4) as u32,
6206 _p0: 0,
6207 };
6208 let zk = gather_backward_zero_kernel(&dev.device);
6209 let u = emit_uniform(std::mem::size_of::<GatherBwdParams>());
6210 let bg = bind_op_output_window(&dev.device, zk, &arena, node.id, &u);
6211 schedule.push(Step::GatherBackward { params: p });
6212 uniforms.push(u);
6213 bind_groups.push(bg);
6214 }
6215 #[cfg(feature = "splat")]
6216 Op::GaussianSplatRender {
6217 width,
6218 height,
6219 tile_size,
6220 radius_scale,
6221 alpha_cutoff,
6222 max_splat_steps,
6223 transmittance_threshold,
6224 max_list_entries,
6225 } => {
6226 let elem_len = |id: NodeId| -> u32 {
6227 graph.node(id).shape.num_elements().unwrap_or(0) as u32
6228 };
6229 schedule.push(Step::GaussianSplatRender {
6230 positions_byte_off: arena.offset(node.inputs[0]) as u32,
6231 positions_len: elem_len(node.inputs[0]),
6232 scales_byte_off: arena.offset(node.inputs[1]) as u32,
6233 scales_len: elem_len(node.inputs[1]),
6234 rotations_byte_off: arena.offset(node.inputs[2]) as u32,
6235 rotations_len: elem_len(node.inputs[2]),
6236 opacities_byte_off: arena.offset(node.inputs[3]) as u32,
6237 opacities_len: elem_len(node.inputs[3]),
6238 colors_byte_off: arena.offset(node.inputs[4]) as u32,
6239 colors_len: elem_len(node.inputs[4]),
6240 sh_coeffs_byte_off: arena.offset(node.inputs[5]) as u32,
6241 sh_coeffs_len: elem_len(node.inputs[5]),
6242 meta_byte_off: arena.offset(node.inputs[6]) as u32,
6243 dst_byte_off: arena.offset(node.id) as u32,
6244 dst_len: node.shape.num_elements().unwrap_or(0) as u32,
6245 width: *width,
6246 height: *height,
6247 tile_size: *tile_size,
6248 radius_scale: *radius_scale,
6249 alpha_cutoff: *alpha_cutoff,
6250 max_splat_steps: *max_splat_steps,
6251 transmittance_threshold: *transmittance_threshold,
6252 max_list_entries: *max_list_entries,
6253 });
6254 }
6255
6256 #[cfg(feature = "splat")]
6257 Op::GaussianSplatRenderBackward {
6258 width,
6259 height,
6260 tile_size,
6261 radius_scale,
6262 alpha_cutoff,
6263 max_splat_steps,
6264 transmittance_threshold,
6265 max_list_entries,
6266 loss_grad_clip,
6267 sh_band,
6268 max_anisotropy,
6269 } => {
6270 let elem_len = |id: NodeId| -> u32 {
6271 graph.node(id).shape.num_elements().unwrap_or(0) as u32
6272 };
6273 schedule.push(Step::GaussianSplatRenderBackward {
6274 positions_byte_off: arena.offset(node.inputs[0]) as u32,
6275 positions_len: elem_len(node.inputs[0]),
6276 scales_byte_off: arena.offset(node.inputs[1]) as u32,
6277 scales_len: elem_len(node.inputs[1]),
6278 rotations_byte_off: arena.offset(node.inputs[2]) as u32,
6279 rotations_len: elem_len(node.inputs[2]),
6280 opacities_byte_off: arena.offset(node.inputs[3]) as u32,
6281 opacities_len: elem_len(node.inputs[3]),
6282 colors_byte_off: arena.offset(node.inputs[4]) as u32,
6283 colors_len: elem_len(node.inputs[4]),
6284 sh_coeffs_byte_off: arena.offset(node.inputs[5]) as u32,
6285 sh_coeffs_len: elem_len(node.inputs[5]),
6286 meta_byte_off: arena.offset(node.inputs[6]) as u32,
6287 d_loss_byte_off: arena.offset(node.inputs[7]) as u32,
6288 d_loss_len: elem_len(node.inputs[7]),
6289 packed_byte_off: arena.offset(node.id) as u32,
6290 packed_len: node.shape.num_elements().unwrap_or(0) as u32,
6291 width: *width,
6292 height: *height,
6293 tile_size: *tile_size,
6294 radius_scale: *radius_scale,
6295 alpha_cutoff: *alpha_cutoff,
6296 max_splat_steps: *max_splat_steps,
6297 transmittance_threshold: *transmittance_threshold,
6298 max_list_entries: *max_list_entries,
6299 loss_grad_clip: *loss_grad_clip,
6300 sh_band: *sh_band,
6301 max_anisotropy: *max_anisotropy,
6302 });
6303 }
6304
6305 #[cfg(feature = "splat")]
6306 Op::GaussianSplatPrepare {
6307 width,
6308 height,
6309 tile_size,
6310 radius_scale,
6311 alpha_cutoff,
6312 max_splat_steps,
6313 transmittance_threshold,
6314 max_list_entries,
6315 } => {
6316 let elem_len = |id: NodeId| -> u32 {
6317 graph.node(id).shape.num_elements().unwrap_or(0) as u32
6318 };
6319 schedule.push(Step::GaussianSplatPrepare {
6320 positions_byte_off: arena.offset(node.inputs[0]) as u32,
6321 positions_len: elem_len(node.inputs[0]),
6322 scales_byte_off: arena.offset(node.inputs[1]) as u32,
6323 scales_len: elem_len(node.inputs[1]),
6324 rotations_byte_off: arena.offset(node.inputs[2]) as u32,
6325 rotations_len: elem_len(node.inputs[2]),
6326 opacities_byte_off: arena.offset(node.inputs[3]) as u32,
6327 opacities_len: elem_len(node.inputs[3]),
6328 colors_byte_off: arena.offset(node.inputs[4]) as u32,
6329 colors_len: elem_len(node.inputs[4]),
6330 sh_coeffs_byte_off: arena.offset(node.inputs[5]) as u32,
6331 sh_coeffs_len: elem_len(node.inputs[5]),
6332 meta_byte_off: arena.offset(node.inputs[6]) as u32,
6333 meta_len: elem_len(node.inputs[6]),
6334 prep_byte_off: arena.offset(node.id) as u32,
6335 prep_len: node.shape.num_elements().unwrap_or(0) as u32,
6336 width: *width,
6337 height: *height,
6338 tile_size: *tile_size,
6339 radius_scale: *radius_scale,
6340 alpha_cutoff: *alpha_cutoff,
6341 max_splat_steps: *max_splat_steps,
6342 transmittance_threshold: *transmittance_threshold,
6343 max_list_entries: *max_list_entries,
6344 });
6345 }
6346
6347 #[cfg(feature = "splat")]
6348 Op::GaussianSplatRasterize {
6349 width,
6350 height,
6351 tile_size,
6352 alpha_cutoff,
6353 max_splat_steps,
6354 transmittance_threshold,
6355 max_list_entries,
6356 } => {
6357 let elem_len = |id: NodeId| -> u32 {
6358 graph.node(id).shape.num_elements().unwrap_or(0) as u32
6359 };
6360 let prep_id = node.inputs[0];
6361 let count = match &graph.node(prep_id).op {
6362 rlx_ir::Op::GaussianSplatPrepare { .. } => {
6363 elem_len(graph.node(prep_id).inputs[0]) / 3
6364 }
6365 _ => 1,
6366 };
6367 schedule.push(Step::GaussianSplatRasterize {
6368 prep_byte_off: arena.offset(prep_id) as u32,
6369 prep_len: elem_len(prep_id),
6370 meta_byte_off: arena.offset(node.inputs[1]) as u32,
6371 meta_len: elem_len(node.inputs[1]),
6372 dst_byte_off: arena.offset(node.id) as u32,
6373 dst_len: node.shape.num_elements().unwrap_or(0) as u32,
6374 count,
6375 width: *width,
6376 height: *height,
6377 tile_size: *tile_size,
6378 alpha_cutoff: *alpha_cutoff,
6379 max_splat_steps: *max_splat_steps,
6380 transmittance_threshold: *transmittance_threshold,
6381 max_list_entries: *max_list_entries,
6382 });
6383 }
6384
6385 Op::If { .. } | Op::While { .. } => {
6386 panic!(
6391 "rlx-wgpu: Op::If/While leaked past unfusion pass — \
6392 check unfuse.rs::expand_if / expand_while"
6393 );
6394 }
6395 Op::RngNormal {
6396 mean,
6397 scale,
6398 key,
6399 op_seed,
6400 } => {
6401 let len = node.shape.num_elements().unwrap_or(0) as u32;
6402 schedule.push(Step::RngNormalHost {
6403 dst_byte_off: arena.offset(node.id) as u32,
6404 len,
6405 mean: *mean,
6406 scale: *scale,
6407 key: *key,
6408 op_seed: *op_seed,
6409 });
6410 }
6411 Op::RngUniform {
6412 low,
6413 high,
6414 key,
6415 op_seed,
6416 } => {
6417 let len = node.shape.num_elements().unwrap_or(0) as u32;
6418 schedule.push(Step::RngUniformHost {
6419 dst_byte_off: arena.offset(node.id) as u32,
6420 len,
6421 low: *low,
6422 high: *high,
6423 key: *key,
6424 op_seed: *op_seed,
6425 });
6426 }
6427 Op::TransformRegion { steps, .. }
6431 if steps.len() == 1
6432 && matches!(
6433 steps[0],
6434 rlx_ir::op::TransformStep::ResizeNearest2x(
6435 rlx_ir::op::ChainOperand::Input(0)
6436 )
6437 ) =>
6438 {
6439 let in_shape = &graph.node(node.inputs[0]).shape;
6440 schedule.push(Step::ResizeNearest2xHost {
6441 src_byte_off: arena.offset(node.inputs[0]) as u32,
6442 dst_byte_off: arena.offset(node.id) as u32,
6443 n: in_shape.dim(0).unwrap_static() as u32,
6444 c: in_shape.dim(1).unwrap_static() as u32,
6445 h: in_shape.dim(2).unwrap_static() as u32,
6446 w: in_shape.dim(3).unwrap_static() as u32,
6447 });
6448 }
6449 other => panic!(
6450 "rlx-wgpu: op {other:?} not yet lowered (v2 covers Matmul, \
6451 Binary, Compare, Activation, Where — fall back to CPU/Metal/MLX)"
6452 ),
6453 }
6454 }
6455
6456 if rlx_ir::env::flag("RLX_WGPU_SCHEDULE") || rlx_ir::env::flag("RLX_DISPATCH_REPORT") {
6457 let mut counts: std::collections::BTreeMap<&'static str, usize> =
6458 std::collections::BTreeMap::new();
6459 let mut fft_gpu = 0usize;
6460 let mut fft_host = 0usize;
6461 for s in &schedule {
6462 *counts.entry(step_name(s)).or_insert(0) += 1;
6463 match s {
6464 Step::FftGpu { .. } => fft_gpu += 1,
6465 Step::FftHost { .. } => fft_host += 1,
6466 _ => {}
6467 }
6468 }
6469 let arena_mb = arena.size as f64 / (1u64 << 20) as f64;
6470 eprintln!(
6471 "[rlx-wgpu] schedule: {} steps, arena={arena_mb:.1} MiB, fft_gpu={fft_gpu}, fft_host={fft_host}",
6472 schedule.len()
6473 );
6474 for (n, c) in &counts {
6475 eprintln!(" {c:>4} × {n}");
6476 }
6477 }
6478
6479 let coop_f16_vk = schedule_uses_coop_f16_vk(&schedule);
6480
6481 Self {
6482 graph,
6483 arena,
6484 dequant_scratch_off,
6485 schedule,
6486 input_offsets,
6487 param_offsets,
6488 uniforms,
6489 bind_groups,
6490 meta_buffers,
6491 unresolved: None,
6492 last_binding: None,
6493 pending_params: HashMap::new(),
6494 pending_param_bytes: HashMap::new(),
6495 active_extent: None,
6496 uniforms_active_extent: None,
6497 input_staging_hashes: HashMap::new(),
6498 coop_f16_vk,
6499 coop_f16_b_param,
6500 coop_f16_vk_wide_b: HashSet::new(),
6501 coop_f16_vk_wide_bind_groups,
6502 coop_f16_host_activations,
6503 stashed_params: HashMap::new(),
6504 readback_staging: None,
6505 tiny_readback: None,
6506 dispatch_only: false,
6507 fft_gpu_steps,
6508 gpu_handles: HashMap::new(),
6509 gpu_handle_feeds: HashMap::new(),
6510 gpu_handle_resident: HashSet::new(),
6511 pending_read_indices: None,
6512 rng,
6513 }
6514 }
6515
6516 pub fn set_param(&mut self, name: &str, data: &[f32]) {
6517 const STASH_MAX_BYTES: usize = 16 * 1024 * 1024;
6518 if data.len() * 4 <= STASH_MAX_BYTES {
6519 self.stashed_params.insert(name.to_string(), data.to_vec());
6520 }
6521 if self.coop_f16_vk {
6522 crate::coop_f16_vk::refresh_wide_b_flag(&mut self.coop_f16_vk_wide_b, name, data);
6523 }
6524 if self.unresolved.is_some() {
6525 self.pending_params.insert(name.to_string(), data.to_vec());
6526 return;
6527 }
6528 let dev = wgpu_device().expect("rlx-wgpu: device gone");
6529 if let Some(&id) = self.param_offsets.get(name)
6530 && self.arena.has(id)
6531 {
6532 self.arena.write_f32(&dev.queue, id, data);
6533 }
6534 }
6535
6536 pub fn debug_first_nan_node(
6541 &mut self,
6542 inputs: &[(&str, &[f32])],
6543 ) -> Option<(usize, String, String)> {
6544 let _ = self.run(inputs);
6545 let dev = wgpu_device().expect("rlx-wgpu: device gone");
6546 let mut prev_summary = String::from("(none)");
6547 for (i, node) in self.graph.nodes().iter().enumerate() {
6548 if !self.arena.has(node.id) {
6549 continue;
6550 }
6551 let elems = node.shape.num_elements().unwrap_or(0);
6552 if elems == 0 {
6553 continue;
6554 }
6555 let data = self.arena.read_f32(&dev.device, &dev.queue, node.id);
6556 let nan_count = data.iter().filter(|v| v.is_nan()).count();
6557 let inf_count = data.iter().filter(|v| v.is_infinite()).count();
6558 if nan_count > 0 || inf_count > 0 {
6559 return Some((i, format!("{:?}", node.op), prev_summary));
6560 }
6561 let max = data.iter().copied().fold(f32::NEG_INFINITY, f32::max);
6562 let min = data.iter().copied().fold(f32::INFINITY, f32::min);
6563 let abs_max = data.iter().map(|v| v.abs()).fold(0.0_f32, f32::max);
6564 prev_summary = format!(
6565 "node #{i} {:?} shape={:?} min={min:.6e} max={max:.6e} |max|={abs_max:.6e}",
6566 node.op,
6567 node.shape
6568 .dims()
6569 .iter()
6570 .map(|d| format!("{d:?}"))
6571 .collect::<Vec<_>>()
6572 );
6573 }
6574 None
6575 }
6576
6577 pub fn output_dtypes(&self) -> Vec<rlx_ir::DType> {
6581 self.graph
6582 .outputs
6583 .iter()
6584 .map(|&id| self.graph.node(id).shape.dtype())
6585 .collect()
6586 }
6587
6588 pub fn set_param_bytes(&mut self, name: &str, data: &[u8]) {
6593 if self.unresolved.is_some() {
6594 self.pending_param_bytes
6595 .insert(name.to_string(), data.to_vec());
6596 return;
6597 }
6598 let dev = wgpu_device().expect("rlx-wgpu: device gone");
6599 if let Some(&id) = self.param_offsets.get(name)
6600 && self.arena.has(id)
6601 {
6602 dev.queue
6603 .write_buffer(&self.arena.buffer, self.arena.offset(id) as u64, data);
6604 }
6605 }
6606
6607 fn dump_node_stats_if_requested(&self, dev: &crate::device::WgpuDevice) {
6608 if !rlx_ir::env::flag("RLX_WGPU_DUMP_NODES") {
6609 return;
6610 }
6611 let flat_probe = rlx_ir::env::parse_or::<usize>("RLX_WGPU_DUMP_FLAT", usize::MAX);
6612 let limit = rlx_ir::env::parse_or("RLX_WGPU_DUMP_NODES_LIMIT", 40usize);
6613 eprintln!(
6614 "[rlx-wgpu-dump] per-node max |x| (topo order, limit={limit}{})",
6615 if flat_probe != usize::MAX {
6616 format!(", flat[{flat_probe}]")
6617 } else {
6618 String::new()
6619 }
6620 );
6621 let mut shown = 0usize;
6622 for (i, node) in self.graph.nodes().iter().enumerate() {
6623 if !self.arena.has(node.id) {
6624 continue;
6625 }
6626 if matches!(
6627 node.op,
6628 rlx_ir::Op::Input { .. }
6629 | rlx_ir::Op::Param { .. }
6630 | rlx_ir::Op::Constant { .. }
6631 | rlx_ir::Op::Reshape { .. }
6632 | rlx_ir::Op::Cast { .. }
6633 ) {
6634 continue;
6635 }
6636 let data = self.arena.read_f32(&dev.device, &dev.queue, node.id);
6637 let max = data.iter().fold(0.0f32, |m, &v| m.max(v.abs()));
6638 let nz = data.iter().filter(|&&v| v != 0.0).count();
6639 let flat_s = if flat_probe < data.len() {
6640 format!(" flat[{flat_probe}]={:.6}", data[flat_probe])
6641 } else {
6642 String::new()
6643 };
6644 eprintln!(
6645 " [{i:>3}] {:?} max={max:.6} nonzero={}/{}{flat_s}",
6646 node.op,
6647 nz,
6648 data.len()
6649 );
6650 shown += 1;
6651 if shown >= limit {
6652 break;
6653 }
6654 }
6655 }
6656
6657 pub fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
6658 self.run_read_outputs(inputs, None)
6659 }
6660
6661 pub fn run_read_outputs(
6662 &mut self,
6663 inputs: &[(&str, &[f32])],
6664 read_indices: Option<&[usize]>,
6665 ) -> Vec<Vec<f32>> {
6666 self.pending_read_indices = read_indices.map(|s| s.to_vec());
6667 let outs = self.run_inner(inputs);
6668 self.pending_read_indices = None;
6669 outs
6670 }
6671
6672 #[cfg(target_arch = "wasm32")]
6682 pub async fn run_async(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
6683 assert!(
6684 !self
6685 .schedule
6686 .iter()
6687 .any(|s| step_runs_on_host(s) || step_is_tail_host(s)),
6688 "rlx-wgpu run_async: graph contains host-executed ops unsupported on wasm"
6689 );
6690
6691 self.dispatch_only = true;
6693 let _ = self.run_inner(inputs);
6694 self.dispatch_only = false;
6695
6696 let dev =
6697 wgpu_device().expect("rlx-wgpu: device not initialized (call init_wgpu_device first)");
6698
6699 let plan = self.readback_plan();
6701 let out_ids_all: Vec<_> = self.graph.outputs.clone();
6702 let out_ids: Vec<_> = plan.iter().map(|&i| out_ids_all[i]).collect();
6703 let layout = ReadbackLayout::for_nodes(&self.arena, &out_ids);
6704 if layout.regions.is_empty() {
6705 return Vec::new();
6706 }
6707 ReadbackStaging::prepare(&dev.device, &mut self.readback_staging, layout.total_bytes);
6708 let staging_buf = self
6709 .readback_staging
6710 .as_ref()
6711 .expect("readback staging")
6712 .buffer()
6713 .clone();
6714
6715 let mut enc = dev
6716 .device
6717 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
6718 label: Some("rlx-wgpu async readback"),
6719 });
6720 encode_readback_copies(&mut enc, &self.arena, &staging_buf, &out_ids, &layout);
6721 dev.queue.submit(std::iter::once(enc.finish()));
6722
6723 crate::buffer::wasm_async::map_read_async(&staging_buf, layout.total_bytes)
6724 .await
6725 .expect("rlx-wgpu async buffer map failed");
6726
6727 let partial = decode_mapped_readback_f32(&staging_buf, &layout);
6728 self.pack_readback_outputs(&plan, partial)
6729 }
6730
6731 pub fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
6732 if !self.input_offsets.contains_key(name) {
6733 return false;
6734 }
6735 self.gpu_handle_resident.remove(name);
6736 self.gpu_handles.insert(name.to_string(), data.to_vec());
6737 true
6738 }
6739
6740 pub fn has_gpu_handle(&self, name: &str) -> bool {
6741 self.gpu_handles.contains_key(name)
6742 }
6743
6744 pub fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) {
6745 self.gpu_handle_feeds
6746 .insert(handle_name.to_string(), output_index);
6747 }
6748
6749 pub fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
6750 if let Some(&out_idx) = self.gpu_handle_feeds.get(name) {
6751 if out_idx < self.graph.outputs.len() {
6752 let id = self.graph.outputs[out_idx];
6753 if self.arena.has(id) {
6754 let dev = wgpu_device().expect("rlx-wgpu: device gone");
6755 return Some(self.arena.read_f32(&dev.device, &dev.queue, id));
6756 }
6757 }
6758 }
6759 if self.gpu_handle_resident.contains(name) {
6760 if let Some(&id) = self.input_offsets.get(name) {
6761 if self.arena.has(id) {
6762 let dev = wgpu_device().expect("rlx-wgpu: device gone");
6763 return Some(self.arena.read_f32(&dev.device, &dev.queue, id));
6764 }
6765 }
6766 }
6767 self.gpu_handles.get(name).cloned()
6768 }
6769
6770 pub fn clone_for_cache(&self) -> Self {
6772 let graph = self
6773 .unresolved
6774 .clone()
6775 .unwrap_or_else(|| self.graph.clone());
6776 let mut exe = Self::compile_rng(graph, self.rng());
6777 for (k, v) in &self.stashed_params {
6778 exe.set_param(k, v);
6779 }
6780 for (k, v) in &self.pending_params {
6781 exe.set_param(k, v);
6782 }
6783 for (k, v) in &self.pending_param_bytes {
6784 exe.set_param_bytes(k, v);
6785 }
6786 for (k, v) in &self.gpu_handles {
6787 exe.bind_gpu_handle(k, v);
6788 }
6789 for (k, &idx) in &self.gpu_handle_feeds {
6790 exe.set_gpu_handle_feed(k, idx);
6791 }
6792 exe.set_active_extent(self.active_extent);
6793 exe.set_rng(self.rng());
6794 exe
6795 }
6796
6797 fn readback_plan(&self) -> Vec<usize> {
6798 let n = self.graph.outputs.len();
6799 if self.pending_read_indices.is_none() && self.gpu_handle_feeds.is_empty() {
6800 return (0..n).collect();
6801 }
6802 if let Some(ref want) = self.pending_read_indices {
6803 let mut v: Vec<_> = want.to_vec();
6804 v.sort_unstable();
6805 return v;
6806 }
6807 (0..n).collect()
6808 }
6809
6810 fn propagate_gpu_handle_feeds_on_gpu(
6811 &mut self,
6812 dev: &crate::device::WgpuDevice,
6813 enc: &mut wgpu::CommandEncoder,
6814 ) {
6815 let extent = self.active_extent;
6816 let feeds: Vec<(String, usize)> = self
6817 .gpu_handle_feeds
6818 .iter()
6819 .map(|(n, &i)| (n.clone(), i))
6820 .collect();
6821 for (name, out_idx) in feeds {
6822 if out_idx >= self.graph.outputs.len() {
6823 continue;
6824 }
6825 let out_id = self.graph.outputs[out_idx];
6826 let Some(&in_id) = self.input_offsets.get(name.as_str()) else {
6827 continue;
6828 };
6829 if in_id != out_id {
6830 let out_bytes = self.arena.len_of(out_id);
6831 let copy_bytes = match extent {
6832 Some((actual, upper)) if upper > 0 => {
6833 let stride = (out_bytes / (upper + 1)).max(4);
6834 (actual * stride).min(out_bytes)
6835 }
6836 _ => out_bytes,
6837 };
6838 self.dispatch_arena_copy_bytes(dev, enc, out_id, in_id, copy_bytes);
6839 }
6840 self.gpu_handle_resident.insert(name.clone());
6841 self.gpu_handles.insert(name.clone(), Vec::new());
6842 }
6843 }
6844
6845 fn dispatch_arena_copy_bytes(
6846 &self,
6847 dev: &crate::device::WgpuDevice,
6848 enc: &mut wgpu::CommandEncoder,
6849 src_id: NodeId,
6850 dst_id: NodeId,
6851 nbytes: usize,
6852 ) {
6853 if nbytes == 0 {
6854 return;
6855 }
6856 let src = self.arena.offset(src_id) as u64;
6857 let dst = self.arena.offset(dst_id) as u64;
6858 let nbytes = nbytes
6859 .min(self.arena.len_of(src_id))
6860 .min(self.arena.len_of(dst_id)) as u64;
6861 let elems = (nbytes / 4).max(1) as u32;
6862 let lo = src.min(dst);
6863 let hi = src.saturating_add(nbytes).max(dst.saturating_add(nbytes));
6864 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
6865 let mut size = hi.saturating_sub(lo).div_ceil(256) * 256;
6866 size = size.max(256).min(max_binding);
6867 let mut base = (lo / 256) * 256;
6868 if base.saturating_add(size) > self.arena.size as u64 {
6869 base = (self.arena.size as u64).saturating_sub(size);
6870 base = (base / 256) * 256;
6871 }
6872 let p = CopyParams {
6873 n: elems,
6874 in_off: (src.saturating_sub(base) / 4) as u32,
6875 out_off: (dst.saturating_sub(base) / 4) as u32,
6876 _p0: 0,
6877 _p1: 0,
6878 _p2: 0,
6879 _p3: 0,
6880 _p4: 0,
6881 };
6882 let ck = copy_kernel(&dev.device);
6883 let u = dev.device.create_buffer(&wgpu::BufferDescriptor {
6884 label: Some("rlx-wgpu kv_feed_copy uniform"),
6885 size: std::mem::size_of::<CopyParams>() as u64,
6886 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
6887 mapped_at_creation: false,
6888 });
6889 dev.queue.write_buffer(&u, 0, bytemuck::bytes_of(&p));
6890 let bg = bind_two_buf0_window(&dev.device, ck, &self.arena.buffer, base, size, &u);
6891 let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
6892 label: Some("rlx-wgpu kv_feed_copy pass"),
6893 ..Default::default()
6894 });
6895 pass.set_pipeline(&ck.pipeline);
6896 pass.set_bind_group(0, &bg, &[]);
6897 let (gx, gy, gz) = dispatch_dims(elems, 64);
6898 pass.dispatch_workgroups(gx, gy, gz);
6899 }
6900
6901 #[allow(dead_code)]
6902 fn dispatch_arena_copy_between_nodes(
6903 &self,
6904 dev: &crate::device::WgpuDevice,
6905 enc: &mut wgpu::CommandEncoder,
6906 src_id: NodeId,
6907 dst_id: NodeId,
6908 ) {
6909 let nbytes = self.arena.len_of(src_id).min(self.arena.len_of(dst_id));
6910 self.dispatch_arena_copy_bytes(dev, enc, src_id, dst_id, nbytes);
6911 }
6912
6913 fn stage_gpu_handle_inputs(
6914 &mut self,
6915 dev: &crate::device::WgpuDevice,
6916 inputs: &[(&str, &[f32])],
6917 ) {
6918 for (name, data) in &self.gpu_handles {
6919 if self.gpu_handle_resident.contains(name) || inputs.iter().any(|(n, _)| n == name) {
6920 continue;
6921 }
6922 if let Some(&id) = self.input_offsets.get(name.as_str())
6923 && self.arena.has(id)
6924 {
6925 self.arena.write_f32(&dev.queue, id, data);
6926 self.input_staging_hashes.remove(name);
6927 }
6928 }
6929 }
6930
6931 fn pack_readback_outputs(&mut self, plan: &[usize], partial: Vec<Vec<f32>>) -> Vec<Vec<f32>> {
6932 if self.pending_read_indices.is_none() {
6933 for (pos, &out_i) in plan.iter().enumerate() {
6934 if let Some(data) = partial.get(pos) {
6935 for (name, &feed_i) in &self.gpu_handle_feeds {
6936 if feed_i == out_i {
6937 self.gpu_handles.insert(name.clone(), data.clone());
6938 }
6939 }
6940 }
6941 }
6942 }
6943 if self.pending_read_indices.is_none() && plan.len() == self.graph.outputs.len() {
6944 return partial;
6945 }
6946 let want = self.pending_read_indices.as_deref().unwrap_or(plan);
6947 let mut by_idx = std::collections::HashMap::new();
6948 for (pos, &i) in plan.iter().enumerate() {
6949 if let Some(d) = partial.get(pos) {
6950 by_idx.insert(i, d.clone());
6951 }
6952 }
6953 want.iter()
6954 .map(|&i| {
6955 by_idx
6956 .get(&i)
6957 .cloned()
6958 .expect("readback plan missing output")
6959 })
6960 .collect()
6961 }
6962
6963 fn run_tail_host_audio_ops(&self, dev: &crate::device::WgpuDevice) {
6964 if !self.schedule.iter().any(step_is_tail_host) {
6965 return;
6966 }
6967 for step in &self.schedule {
6968 if !step_is_tail_host(step) {
6969 continue;
6970 }
6971 match step {
6972 Step::WelchPeaksHost {
6973 spec_byte_off,
6974 dst_byte_off,
6975 welch_batch,
6976 n_fft,
6977 n_segments,
6978 k,
6979 } => {
6980 crate::welch_peaks_host::run_welch_peaks(
6981 &self.arena,
6982 &dev.device,
6983 &dev.queue,
6984 *spec_byte_off as usize,
6985 *dst_byte_off as usize,
6986 *welch_batch as usize,
6987 *n_fft as usize,
6988 *n_segments as usize,
6989 *k as usize,
6990 );
6991 }
6992 Step::LogMelHost {
6993 spec_byte_off,
6994 filt_byte_off,
6995 dst_byte_off,
6996 outer,
6997 n_fft,
6998 n_bins,
6999 n_mels,
7000 } => {
7001 crate::log_mel_host::run_log_mel(
7002 &self.arena,
7003 &dev.device,
7004 &dev.queue,
7005 *spec_byte_off as usize,
7006 *filt_byte_off as usize,
7007 *dst_byte_off as usize,
7008 *outer as usize,
7009 *n_fft as usize,
7010 *n_bins as usize,
7011 *n_mels as usize,
7012 );
7013 }
7014 Step::LogMelBackwardHost {
7015 spec_byte_off,
7016 filt_byte_off,
7017 dy_byte_off,
7018 dst_byte_off,
7019 outer,
7020 n_fft,
7021 n_bins,
7022 n_mels,
7023 } => {
7024 crate::log_mel_host::run_log_mel_backward(
7025 &self.arena,
7026 &dev.device,
7027 &dev.queue,
7028 *spec_byte_off as usize,
7029 *filt_byte_off as usize,
7030 *dy_byte_off as usize,
7031 *dst_byte_off as usize,
7032 *outer as usize,
7033 *n_fft as usize,
7034 *n_bins as usize,
7035 *n_mels as usize,
7036 );
7037 }
7038 _ => {}
7039 }
7040 }
7041 }
7042
7043 fn run_inner(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
7044 if self.unresolved.is_some() {
7047 self.lazy_compile_for_inputs(inputs);
7048 }
7049 let dev = wgpu_device().expect("rlx-wgpu: device gone");
7050 self.stage_gpu_handle_inputs(dev, inputs);
7051 let skip_input_upload =
7052 !rlx_ir::env::flag("RLX_WGPU_FORCE_INPUT_UPLOAD") && !self.coop_f16_vk;
7053 for &(name, data) in inputs {
7054 if let Some(&id) = self.input_offsets.get(name)
7055 && self.arena.has(id)
7056 {
7057 if skip_input_upload {
7058 let h = hash_f32_input(data);
7059 if self.input_staging_hashes.get(name) == Some(&h) {
7060 if self.arena.f16_buffer.is_some() {
7061 self.arena.write_f16_shadow(&dev.queue, id, data);
7062 }
7063 continue;
7064 }
7065 self.arena.write_f32(&dev.queue, id, data);
7066 self.input_staging_hashes.insert(name.to_string(), h);
7067 } else {
7068 self.arena.write_f32(&dev.queue, id, data);
7069 }
7070 }
7071 }
7072 for &(act_id, act, ref src_name) in &self.coop_f16_host_activations {
7073 let src =
7074 host_tensor_f32(src_name, inputs, &self.stashed_params).unwrap_or_else(|| {
7075 panic!("rlx-wgpu CoopF16Vk host activation: missing tensor {src_name:?}")
7076 });
7077 let mirrored = apply_activation_host(act, src);
7078 self.arena.write_f32(&dev.queue, act_id, &mirrored);
7079 }
7080 if !self.coop_f16_host_activations.is_empty() {
7081 let flush = dev
7083 .device
7084 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
7085 label: Some("rlx-wgpu host mirror flush"),
7086 });
7087 dev.queue.submit(std::iter::once(flush.finish()));
7088 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
7089 }
7090
7091 let active = self.active_extent.filter(|_| self.all_safe_for_active());
7096 let scale = |full: u32| -> u32 {
7097 match active {
7098 Some((a, u)) if u > 0 => {
7099 let f = full as usize;
7100 (f * a).div_ceil(u).min(f) as u32
7101 }
7102 _ => full,
7103 }
7104 };
7105
7106 let need_uniform_writes = self.uniforms_active_extent != Some(active);
7112 if need_uniform_writes {
7113 let mut gpu_ui = 0usize;
7114 for step in self.schedule.iter() {
7115 if step_runs_on_host(step) {
7116 continue;
7117 }
7118 match step {
7119 Step::CastF32ToF16 { .. } => {
7120 }
7124 Step::Matmul {
7125 m,
7126 k,
7127 n,
7128 a_off_f32,
7129 b_off_f32,
7130 c_off_f32,
7131 batch,
7132 a_batch_stride,
7133 b_batch_stride,
7134 c_batch_stride,
7135 has_bias,
7136 bias_off_f32,
7137 act_id,
7138 b_is_param: _,
7139 compute_precision: _,
7140 } => {
7141 let m_scaled = scale(*m);
7145 let p = MatmulParams {
7146 m: m_scaled,
7147 k: *k,
7148 n: *n,
7149 a_off: *a_off_f32,
7150 b_off: *b_off_f32,
7151 c_off: *c_off_f32,
7152 batch: *batch,
7153 a_batch_stride: *a_batch_stride,
7154 b_batch_stride: *b_batch_stride,
7155 c_batch_stride: *c_batch_stride,
7156 has_bias: *has_bias,
7157 bias_off: *bias_off_f32,
7158 act_id: *act_id,
7159 _pad0: 0,
7160 _pad1: 0,
7161 _pad2: 0,
7162 };
7163 dev.queue
7164 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7165 }
7166 Step::Binary { params } | Step::Compare { params } => {
7167 let mut p = *params;
7168 p.n = scale(p.n);
7169 dev.queue
7170 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7171 }
7172 Step::Unary { params, .. } => {
7173 let mut p = *params;
7174 p.n = scale(p.n);
7175 dev.queue
7176 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7177 }
7178 Step::Where { params } => {
7179 let mut p = *params;
7180 p.n = scale(p.n);
7181 dev.queue
7182 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7183 }
7184 Step::Fma { params } => {
7185 let mut p = *params;
7186 p.n = scale(p.n);
7187 dev.queue
7188 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7189 }
7190 Step::Reduce { params } => {
7191 let mut p = *params;
7192 p.outer = scale(p.outer);
7193 dev.queue
7194 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7195 }
7196 Step::Softmax { params } => {
7197 let mut p = *params;
7198 p.outer = scale(p.outer);
7199 dev.queue
7200 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7201 }
7202 Step::SoftmaxCrossEntropy { params } => {
7203 let mut p = *params;
7204 p.outer = scale(p.outer);
7205 dev.queue
7206 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7207 }
7208 Step::LayerNorm { params } => {
7209 let mut p = *params;
7210 p.outer = scale(p.outer);
7211 dev.queue
7212 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7213 }
7214 Step::RmsNormBackwardInput { params }
7215 | Step::RmsNormBackwardGamma { params }
7216 | Step::RmsNormBackwardBeta { params } => {
7217 let mut p = *params;
7218 p.outer = scale(p.outer);
7219 dev.queue
7220 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7221 }
7222 Step::LayerNormBackwardInput { params } => {
7223 let mut p = *params;
7224 p.outer = scale(p.outer);
7225 dev.queue
7226 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7227 }
7228 Step::LayerNormBackwardGammaPartial { params, .. } => {
7229 let mut p = *params;
7230 p.outer = scale(p.outer);
7231 dev.queue
7232 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7233 }
7234 Step::LayerNormBackwardGammaReduce { params } => {
7235 dev.queue.write_buffer(
7239 &self.uniforms[gpu_ui],
7240 0,
7241 bytemuck::bytes_of(params),
7242 );
7243 }
7244 Step::CumsumBackward { params } => {
7245 let mut p = *params;
7246 p.outer = scale(p.outer);
7247 dev.queue
7248 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7249 }
7250 Step::RopeBackward { params } => {
7251 let mut p = *params;
7252 p.seq = scale(p.seq);
7253 dev.queue
7254 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7255 }
7256 Step::GatherBackward { params } => {
7257 let mut p = *params;
7258 p.outer = scale(p.outer);
7259 dev.queue
7260 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7261 }
7262 Step::Cumsum { params } => {
7263 let mut p = *params;
7264 p.outer = scale(p.outer);
7265 dev.queue
7266 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7267 }
7268 Step::FftGpu { .. } => {}
7269 Step::Copy { params } => {
7270 let mut p = *params;
7271 p.n = scale(p.n);
7272 dev.queue
7273 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7274 }
7275 Step::BufferCopy { .. } => {}
7276 Step::ElementwiseRegion { params } => {
7277 let mut p = *params;
7279 p.len = scale(p.len);
7280 dev.queue
7281 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7282 }
7283 Step::BatchElementwiseRegion { params } => {
7284 let mut p = *params;
7285 p.slice_len = scale(p.slice_len);
7286 p.num_batch = scale(p.num_batch);
7287 dev.queue
7288 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7289 }
7290 Step::Transpose { params, .. } => {
7291 let mut p = *params;
7296 if p.bucket_outermost == 1 && p.out_dim_0 > 0 {
7297 let scaled_d0 = scale(p.out_dim_0);
7298 let inner = p.out_total / p.out_dim_0;
7299 p.out_total = scaled_d0 * inner;
7300 }
7301 dev.queue
7302 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7303 }
7304 Step::Narrow { params } => {
7305 let mut p = *params;
7306 p.total = scale(p.total);
7307 dev.queue
7308 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7309 }
7310 Step::Concat { params } => {
7311 let mut p = *params;
7312 p.total = scale(p.total);
7313 dev.queue
7314 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7315 }
7316 Step::Gather { params } => {
7317 let mut p = *params;
7318 p.n_out = scale(p.n_out);
7319 dev.queue
7320 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7321 }
7322 Step::GatherAxis { params } => {
7323 let mut p = *params;
7324 p.total = scale(p.total);
7325 dev.queue
7326 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7327 }
7328 Step::Attention { params, .. } => {
7329 let mut p = *params;
7334 p.seq_q = scale(p.seq_q);
7335 p.seq_k = scale(p.seq_k);
7336 dev.queue
7337 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7338 }
7339 Step::AttentionBackward { params, .. } => {
7340 let mut p = *params;
7341 if p.wrt == 0 {
7342 p.seq_q = scale(p.seq_q);
7343 } else {
7344 p.seq_k = scale(p.seq_k);
7345 }
7346 dev.queue
7347 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7348 }
7349 Step::Rope { params } => {
7350 let mut p = *params;
7355 let s_active = scale(p.seq);
7356 p.seq = s_active;
7357 p.n_total = p.batch * s_active * p.last_dim;
7358 dev.queue
7359 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7360 }
7361 Step::Expand { params, .. } => {
7362 let mut p = *params;
7364 if p.bucket_outermost == 1 && p.out_dim_0 > 0 {
7365 let scaled_d0 = scale(p.out_dim_0);
7366 let inner = p.out_total / p.out_dim_0;
7367 p.out_total = scaled_d0 * inner;
7368 }
7369 dev.queue
7370 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7371 }
7372 Step::Argmax { params } => {
7373 let mut p = *params;
7374 p.outer = scale(p.outer);
7375 dev.queue
7376 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7377 }
7378 Step::Pool2d { params } => {
7379 let mut p = *params;
7380 p.n = scale(p.n);
7381 dev.queue
7382 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7383 }
7384 Step::Conv2d { params } => {
7385 let mut p = *params;
7386 p.n = scale(p.n);
7387 dev.queue
7388 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7389 }
7390 Step::Pool1d { params } => {
7391 let mut p = *params;
7392 p.n = scale(p.n);
7393 dev.queue
7394 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7395 }
7396 Step::Pool3d { params } => {
7397 let mut p = *params;
7398 p.n = scale(p.n);
7399 dev.queue
7400 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7401 }
7402 Step::Conv1d { params } => {
7403 let mut p = *params;
7404 p.n = scale(p.n);
7405 dev.queue
7406 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7407 }
7408 Step::Conv3d { params } => {
7409 let mut p = *params;
7410 p.n = scale(p.n);
7411 dev.queue
7412 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7413 }
7414 Step::ScatterAdd { params } => {
7415 let mut p = *params;
7419 if p.op == 1 {
7420 p.num_updates = scale(p.num_updates);
7421 }
7422 dev.queue
7423 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7424 }
7425 Step::TopK { params } => {
7426 let mut p = *params;
7427 p.outer = scale(p.outer);
7428 dev.queue
7429 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7430 }
7431 Step::WelchPeaksGpu { params } => {
7432 let mut p = *params;
7433 p.welch_batch = scale(p.welch_batch);
7434 dev.queue
7435 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7436 }
7437 Step::UmapKnn { params } => {
7438 let mut p = *params;
7439 p.n = scale(p.n);
7440 dev.queue
7441 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7442 }
7443 Step::GroupedMatmul { params } => {
7444 let mut p = *params;
7445 p.m = scale(p.m);
7446 dev.queue
7447 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7448 }
7449 Step::Sample { params } => {
7450 let mut p = *params;
7451 p.outer = scale(p.outer);
7452 dev.queue
7453 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7454 }
7455 Step::SelectiveScan { params } => {
7456 let mut p = *params;
7458 p.seq = scale(p.seq);
7459 dev.queue
7460 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7461 }
7462 Step::Mamba2 { params } => {
7463 let mut p = *params;
7464 p.seq = scale(p.seq);
7465 dev.queue
7466 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7467 }
7468 Step::Gru { params } => {
7469 let mut p = *params;
7470 p.seq = scale(p.seq);
7471 dev.queue
7472 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7473 }
7474 Step::Rnn { params } => {
7475 let mut p = *params;
7476 p.seq = scale(p.seq);
7477 dev.queue
7478 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7479 }
7480 Step::DequantMatmul { params } => {
7481 let mut p = *params;
7482 p.m = scale(p.m);
7483 dev.queue
7484 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7485 }
7486 Step::GatherSplit { .. }
7487 | Step::DequantMatmulGguf { .. }
7488 | Step::DequantGroupedMatmulGguf { .. }
7489 | Step::GatedDeltaNet { .. }
7490 | Step::Lstm { .. }
7491 | Step::ConvTranspose2d { .. }
7492 | Step::GroupNormHost { .. }
7493 | Step::LayerNorm2dHost { .. }
7494 | Step::ResizeNearest2xHost { .. }
7495 | Step::ReverseHost { .. }
7496 | Step::ArgReduceHost { .. }
7497 | Step::GruHost { .. }
7498 | Step::RnnHost { .. }
7499 | Step::Llada2GroupLimitedGate { .. }
7500 | Step::UmapKnnHost { .. }
7501 | Step::MsDeformAttnHost { .. }
7502 | Step::FftHost { .. }
7503 | Step::Im2ColHost { .. }
7504 | Step::RngNormalHost { .. }
7505 | Step::RngUniformHost { .. }
7506 | Step::WelchPeaksHost { .. }
7507 | Step::LogMelHost { .. }
7508 | Step::LogMelBackwardHost { .. } => {}
7509 Step::FusedResidualLn { params } => {
7510 let mut p = *params;
7511 p.outer = scale(p.outer);
7512 dev.queue
7513 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7514 }
7515 Step::FusedResidualLnTee { params } => {
7516 let mut p = *params;
7517 p.outer = scale(p.outer);
7518 dev.queue
7519 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7520 }
7521 Step::FusedResidualRmsNorm { params } => {
7522 let mut p = *params;
7523 p.outer = scale(p.outer);
7524 dev.queue
7525 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7526 }
7527 Step::MatmulQkv { params, kind: _ } => {
7528 let mut p = *params;
7529 p.m = scale(p.m);
7530 dev.queue
7531 .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
7532 }
7533 #[cfg(feature = "splat")]
7534 Step::GaussianSplatRender { .. }
7535 | Step::GaussianSplatRenderBackward { .. }
7536 | Step::GaussianSplatPrepare { .. }
7537 | Step::GaussianSplatRasterize { .. } => {}
7538 }
7539 if !matches!(step, Step::FftGpu { .. }) {
7540 gpu_ui += 1;
7541 }
7542 }
7543 self.uniforms_active_extent = Some(active);
7544 }
7545
7546 let mm_k = matmul_kernel(&dev.device);
7548 let mm_w_active = matmul_wide_active_kernel(&dev.device);
7549 let mm_f16w = matmul_f16w_kernel(&dev.device);
7550 let mm_f16c = matmul_f16_compute_kernel(&dev.device);
7551 let mm_coop = matmul_coop16_kernel(&dev.device);
7552 let mm_coop_f16_vk = matmul_coop_f16_vulkan_kernel(&dev.device);
7553 let mm_coop_f32 = matmul_coop_f32_active_kernel(&dev.device);
7554 let mm_cast = cast_f32_to_f16_kernel(&dev.device);
7555 let bk = binary_kernel(&dev.device);
7556 let uk = unary_kernel(&dev.device);
7557 let ck = compare_kernel(&dev.device);
7558 let wk = where_kernel(&dev.device);
7559 let fk = fma_kernel(&dev.device);
7560 let mut step_i = 0;
7561 let mut gpu_bi = 0usize;
7562 let mut fft_i = 0usize;
7563 while step_i < self.schedule.len() {
7564 let mut enc = dev
7565 .device
7566 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
7567 label: Some("rlx-wgpu run"),
7568 });
7569 {
7570 let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
7571 label: Some("rlx-wgpu compute pass"),
7572 timestamp_writes: None,
7573 });
7574 let mut pass_dispatched = false;
7575 while step_i < self.schedule.len() {
7576 if step_is_tail_host(&self.schedule[step_i]) {
7577 step_i += 1;
7578 continue;
7579 }
7580 if step_runs_on_host(&self.schedule[step_i]) {
7581 break;
7582 }
7583 if pass_dispatched
7588 && step_i > 0
7589 && step_needs_pass_flush(&self.schedule[step_i], &self.schedule[step_i - 1])
7590 {
7591 break;
7592 }
7593 let step = &self.schedule[step_i];
7594 let _perf = rlx_ir::perfetto::TraceSpan::new(step_name(step), "wgpu");
7597 match step {
7598 Step::CastF32ToF16 { params } => {
7599 if let Some(cast_k) = mm_cast {
7604 pass.set_pipeline(&cast_k.pipeline);
7605 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7606 let (gx, gy, gz) = dispatch_dims(params.len, 64);
7607 pass.dispatch_workgroups(gx, gy, gz);
7608 }
7609 }
7610 Step::Matmul {
7611 m,
7612 n,
7613 batch,
7614 b_off_f32,
7615 b_is_param,
7616 compute_precision,
7617 ..
7618 } =>
7619 {
7626 #[allow(clippy::unnecessary_unwrap)]
7627 let m_s = scale(*m);
7631 if m_s == 0 {
7632 continue;
7633 }
7634 let coop_f16_wide = mm_coop_f16_vk.is_some()
7635 && *compute_precision == MatmulCompute::CoopF16Vk
7636 && crate::coop_f16_vk::use_wide_matmul(
7637 *b_off_f32,
7638 *n,
7639 &self.coop_f16_b_param,
7640 &self.coop_f16_vk_wide_b,
7641 );
7642 pass.set_bind_group(
7643 0,
7644 coop_f16_vk_bind_group(self, gpu_bi, coop_f16_wide),
7645 &[],
7646 );
7647 let f16w_opt_in = rlx_ir::env::flag("RLX_WGPU_F16_WEIGHTS");
7657 if let Some(coop) = mm_coop.as_ref()
7658 && *b_is_param
7659 && *compute_precision == MatmulCompute::Coop16
7660 {
7661 pass.set_pipeline(&coop.pipeline);
7667 pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), *batch);
7668 } else if mm_coop_f16_vk.is_some()
7669 && *compute_precision == MatmulCompute::CoopF16Vk
7670 {
7671 if coop_f16_wide {
7672 dispatch_wide_f32_matmul(
7673 &mut pass,
7674 mm_w_active,
7675 mm_k,
7676 m_s,
7677 *n,
7678 *batch,
7679 );
7680 } else {
7681 let n_eff = scale(*n);
7682 let coop_vk =
7683 matmul_coop_f16_vulkan_active_kernel(&dev.device, n_eff)
7684 .expect("coop f16 vk kernel missing");
7685 pass.set_pipeline(&coop_vk.pipeline);
7686 pass.dispatch_workgroups(
7687 m_s.div_ceil(16),
7688 n.div_ceil(16),
7689 *batch,
7690 );
7691 }
7692 } else if let Some(coop_f32) = mm_coop_f32.as_ref()
7693 && *b_is_param
7694 && *compute_precision == MatmulCompute::CoopF32
7695 {
7696 pass.set_pipeline(&coop_f32.pipeline);
7699 let backend = wgpu_device()
7700 .map(|d| d.backend)
7701 .unwrap_or(wgpu::Backend::Noop);
7702 let (gx, gy) = if backend == wgpu::Backend::Metal {
7703 (n.div_ceil(32), m_s.div_ceil(32))
7704 } else {
7705 (m_s.div_ceil(8), n.div_ceil(8))
7706 };
7707 pass.dispatch_workgroups(gx, gy, *batch);
7708 } else if let Some(f16c) = mm_f16c.as_ref()
7709 && *b_is_param
7710 && *compute_precision == MatmulCompute::F16
7711 {
7712 pass.set_pipeline(&f16c.pipeline);
7713 pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), *batch);
7714 } else if let Some(f16w) = mm_f16w.as_ref()
7715 && *b_is_param
7716 && f16w_opt_in
7717 {
7718 pass.set_pipeline(&f16w.pipeline);
7719 pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), *batch);
7720 } else if m_s >= 32 && *n >= 64 {
7721 pass.set_pipeline(&mm_w_active.pipeline);
7722 let backend = wgpu_device()
7723 .map(|d| d.backend)
7724 .unwrap_or(wgpu::Backend::Noop);
7725 let (gx, gy) = if matches!(
7726 backend,
7727 wgpu::Backend::Vulkan | wgpu::Backend::Dx12
7728 ) {
7729 (n.div_ceil(64), m_s.div_ceil(64))
7730 } else {
7731 (n.div_ceil(64), m_s.div_ceil(32))
7732 };
7733 pass.dispatch_workgroups(gx, gy, *batch);
7734 } else {
7735 pass.set_pipeline(&mm_k.pipeline);
7736 pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), *batch);
7737 }
7738 }
7739 Step::Binary { params } => {
7740 let n_s = scale(params.n);
7741 if n_s == 0 {
7742 continue;
7743 }
7744 pass.set_pipeline(&bk.pipeline);
7745 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7746 let (gx, gy, gz) = dispatch_dims(n_s, 64);
7747 pass.dispatch_workgroups(gx, gy, gz);
7748 }
7749 Step::Compare { params } => {
7750 let n_s = scale(params.n);
7751 if n_s == 0 {
7752 continue;
7753 }
7754 pass.set_pipeline(&ck.pipeline);
7755 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7756 let (gx, gy, gz) = dispatch_dims(n_s, 64);
7757 pass.dispatch_workgroups(gx, gy, gz);
7758 }
7759 Step::Unary { params, f16_mirror } => {
7760 let n_s = scale(params.n);
7761 if n_s == 0 {
7762 continue;
7763 }
7764 if *f16_mirror {
7765 if let Some(uk_f16) = unary_f16_mirror_kernel(&dev.device) {
7766 pass.set_pipeline(&uk_f16.pipeline);
7767 } else {
7768 pass.set_pipeline(&uk.pipeline);
7769 }
7770 } else {
7771 pass.set_pipeline(&uk.pipeline);
7772 }
7773 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7774 let (gx, gy, gz) = dispatch_dims(n_s, 64);
7775 pass.dispatch_workgroups(gx, gy, gz);
7776 }
7777 Step::Where { params } => {
7778 let n_s = scale(params.n);
7779 if n_s == 0 {
7780 continue;
7781 }
7782 pass.set_pipeline(&wk.pipeline);
7783 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7784 let (gx, gy, gz) = dispatch_dims(n_s, 64);
7785 pass.dispatch_workgroups(gx, gy, gz);
7786 }
7787 Step::Fma { params } => {
7788 let n_s = scale(params.n);
7789 if n_s == 0 {
7790 continue;
7791 }
7792 pass.set_pipeline(&fk.pipeline);
7793 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7794 let (gx, gy, gz) = dispatch_dims(n_s, 64);
7795 pass.dispatch_workgroups(gx, gy, gz);
7796 }
7797 Step::Reduce { params } => {
7798 let outer_s = scale(params.outer);
7799 if outer_s == 0 {
7800 continue;
7801 }
7802 let rk = reduce_kernel(&dev.device);
7803 pass.set_pipeline(&rk.pipeline);
7804 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7805 let total_out = outer_s.saturating_mul(params.inner);
7806 if params.reduce_dim <= 64 {
7807 let (gx, gy, gz) = dispatch_dims(total_out, 64);
7809 pass.dispatch_workgroups(gx, gy, gz);
7810 } else {
7811 let (gx, gy, gz) = dispatch_dims(total_out, 1);
7815 pass.dispatch_workgroups(gx, gy, gz);
7816 }
7817 }
7818 Step::Softmax { params } => {
7819 let outer_s = scale(params.outer);
7820 if outer_s == 0 {
7821 continue;
7822 }
7823 let sk = softmax_kernel(&dev.device);
7824 pass.set_pipeline(&sk.pipeline);
7825 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7826 let (gx, gy, gz) = dispatch_dims(outer_s, 64);
7827 pass.dispatch_workgroups(gx, gy, gz);
7828 }
7829 Step::SoftmaxCrossEntropy { params } => {
7830 let outer_s = scale(params.outer);
7831 if outer_s == 0 {
7832 continue;
7833 }
7834 let sk = softmax_cross_entropy_kernel(&dev.device);
7835 pass.set_pipeline(&sk.pipeline);
7836 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7837 let (gx, gy, gz) = dispatch_dims(outer_s, 64);
7838 pass.dispatch_workgroups(gx, gy, gz);
7839 }
7840 Step::LayerNorm { params } => {
7841 let outer_s = scale(params.outer);
7842 if outer_s == 0 {
7843 continue;
7844 }
7845 let lk = layernorm_kernel(&dev.device);
7846 pass.set_pipeline(&lk.pipeline);
7847 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7848 let (gx, gy, gz) = dispatch_dims(outer_s, 64);
7849 pass.dispatch_workgroups(gx, gy, gz);
7850 }
7851 Step::RmsNormBackwardInput { params } => {
7852 let outer_s = scale(params.outer);
7853 if outer_s == 0 {
7854 continue;
7855 }
7856 let rk = rms_norm_backward_kernel(&dev.device);
7857 pass.set_pipeline(&rk.pipeline);
7858 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7859 pass.dispatch_workgroups(outer_s, 1, 1);
7860 }
7861 Step::RmsNormBackwardGamma { params }
7862 | Step::RmsNormBackwardBeta { params } => {
7863 if params.inner == 0 {
7864 continue;
7865 }
7866 let rk = rms_norm_backward_param_kernel(&dev.device);
7867 pass.set_pipeline(&rk.pipeline);
7868 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7869 pass.dispatch_workgroups(1, 1, 1);
7870 }
7871 Step::LayerNormBackwardInput { params } => {
7872 let outer_s = scale(params.outer);
7873 if outer_s == 0 {
7874 continue;
7875 }
7876 let lk = layer_norm_backward_input_kernel(&dev.device);
7877 pass.set_pipeline(&lk.pipeline);
7878 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7879 pass.dispatch_workgroups(outer_s, 1, 1);
7880 }
7881 Step::LayerNormBackwardGammaPartial {
7882 params,
7883 num_workgroups,
7884 } => {
7885 if params.inner == 0 || *num_workgroups == 0 {
7886 continue;
7887 }
7888 let lk = layer_norm_backward_gamma_partial_kernel(&dev.device);
7889 pass.set_pipeline(&lk.pipeline);
7890 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7891 pass.dispatch_workgroups(*num_workgroups, 1, 1);
7892 }
7893 Step::LayerNormBackwardGammaReduce { params } => {
7894 if params.inner == 0 {
7895 continue;
7896 }
7897 let lk = layer_norm_backward_gamma_reduce_kernel(&dev.device);
7898 pass.set_pipeline(&lk.pipeline);
7899 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7900 pass.dispatch_workgroups(1, 1, 1);
7901 }
7902 Step::CumsumBackward { params } => {
7903 let outer_s = scale(params.outer);
7904 if outer_s == 0 {
7905 continue;
7906 }
7907 let ck = cumsum_backward_kernel(&dev.device);
7908 pass.set_pipeline(&ck.pipeline);
7909 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7910 let (gx, gy, gz) = dispatch_dims(outer_s, 64);
7911 pass.dispatch_workgroups(gx, gy, gz);
7912 }
7913 Step::RopeBackward { params } => {
7914 let seq_s = scale(params.seq);
7915 if seq_s == 0 {
7916 continue;
7917 }
7918 let rk = rope_backward_kernel(&dev.device);
7919 pass.set_pipeline(&rk.pipeline);
7920 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7921 let total = params.batch * seq_s * params.hidden;
7922 let (gx, gy, gz) = dispatch_dims(total, 64);
7923 pass.dispatch_workgroups(gx, gy, gz);
7924 }
7925 Step::GatherBackward { params } => {
7926 let outer_s = scale(params.outer);
7927 if outer_s == 0 {
7928 continue;
7929 }
7930 let total = outer_s * params.axis_dim * params.trailing;
7931 if total > 0 {
7932 let zk = gather_backward_zero_kernel(&dev.device);
7933 pass.set_pipeline(&zk.pipeline);
7934 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7935 let (gx, _, _) = dispatch_dims(total, 256);
7936 pass.dispatch_workgroups(gx, 1, 1);
7937 }
7938 let ak = gather_backward_acc_kernel(&dev.device);
7939 pass.set_pipeline(&ak.pipeline);
7940 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7941 pass.dispatch_workgroups(outer_s, 1, 1);
7942 }
7943 Step::Cumsum { params } => {
7944 let outer_s = scale(params.outer);
7945 if outer_s == 0 {
7946 continue;
7947 }
7948 let ck2 = cumsum_kernel(&dev.device);
7949 pass.set_pipeline(&ck2.pipeline);
7950 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7951 let (gx, gy, gz) = dispatch_dims(outer_s, 64);
7952 pass.dispatch_workgroups(gx, gy, gz);
7953 }
7954 Step::FftGpu {
7955 src_off,
7956 dst_off,
7957 outer,
7958 n,
7959 inverse,
7960 norm_scale,
7961 } => {
7962 let res = &self.fft_gpu_steps[fft_i];
7963 fft_i += 1;
7964 crate::fft_dispatch::dispatch_fft_gpu_in_pass(
7965 &dev.device,
7966 &dev.queue,
7967 &mut pass,
7968 res,
7969 *src_off,
7970 *dst_off,
7971 *outer,
7972 *n,
7973 *inverse != 0,
7974 *norm_scale,
7975 );
7976 }
7977 Step::Copy { params } => {
7978 let n_s = scale(params.n);
7979 if n_s == 0 {
7980 continue;
7981 }
7982 let ck2 = copy_kernel(&dev.device);
7983 pass.set_pipeline(&ck2.pipeline);
7984 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7985 let (gx, gy, gz) = dispatch_dims(n_s, 64);
7986 pass.dispatch_workgroups(gx, gy, gz);
7987 }
7988 Step::BufferCopy { .. } => {
7989 }
7991 Step::ElementwiseRegion { params } => {
7992 let len_s = scale(params.len);
7993 if len_s == 0 {
7994 continue;
7995 }
7996 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
7997 if params.prologue == rlx_ir::REGION_PROLOGUE_RESIZE_NEAREST_2X_NCHW {
7998 let ek = elementwise_region_spatial_kernel(&dev.device);
7999 pass.set_pipeline(&ek.pipeline);
8000 let (gx, gy, gz) = dispatch_prologue_nchw(
8001 params.out_w,
8002 params.out_h,
8003 params.out_n * params.out_c,
8004 );
8005 pass.dispatch_workgroups(gx, gy, gz);
8006 } else {
8007 let ek = elementwise_region_kernel(&dev.device);
8008 pass.set_pipeline(&ek.pipeline);
8009 let (gx, gy, gz) = dispatch_dims(len_s, 64);
8010 pass.dispatch_workgroups(gx, gy, gz);
8011 }
8012 }
8013 Step::BatchElementwiseRegion { params } => {
8014 let slice_len_s = scale(params.slice_len);
8015 let num_batch_s = scale(params.num_batch);
8016 if slice_len_s == 0 || num_batch_s == 0 {
8017 continue;
8018 }
8019 let ek = batch_elementwise_region_kernel(&dev.device);
8020 pass.set_pipeline(&ek.pipeline);
8021 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8022 let (gx, gy, _) = dispatch_dims(slice_len_s, 64);
8023 pass.dispatch_workgroups(gx, gy, num_batch_s);
8024 }
8025 Step::Transpose { params, .. } => {
8026 let total_s = if params.bucket_outermost == 1 && params.out_dim_0 > 0 {
8030 let scaled_d0 = scale(params.out_dim_0);
8031 let inner = params.out_total / params.out_dim_0;
8032 scaled_d0 * inner
8033 } else {
8034 params.out_total
8035 };
8036 if total_s == 0 {
8037 continue;
8038 }
8039 let tk = transpose_kernel(&dev.device);
8040 pass.set_pipeline(&tk.pipeline);
8041 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8042 let (gx, gy, gz) = dispatch_dims(total_s, 64);
8043 pass.dispatch_workgroups(gx, gy, gz);
8044 }
8045 Step::Narrow { params } => {
8046 let total_s = scale(params.total);
8047 if total_s == 0 {
8048 continue;
8049 }
8050 let nk = narrow_kernel(&dev.device);
8051 pass.set_pipeline(&nk.pipeline);
8052 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8053 let (gx, gy, gz) = dispatch_dims(total_s, 64);
8054 pass.dispatch_workgroups(gx, gy, gz);
8055 }
8056 Step::Concat { params } => {
8057 let total_s = scale(params.total);
8058 if total_s == 0 {
8059 continue;
8060 }
8061 let cck = concat_kernel(&dev.device);
8062 pass.set_pipeline(&cck.pipeline);
8063 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8064 let (gx, gy, gz) = dispatch_dims(total_s, 64);
8065 pass.dispatch_workgroups(gx, gy, gz);
8066 }
8067 Step::Gather { params } => {
8068 let n_out_s = scale(params.n_out);
8069 if n_out_s == 0 {
8070 continue;
8071 }
8072 let gk = gather_kernel(&dev.device);
8073 pass.set_pipeline(&gk.pipeline);
8074 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8075 let (gx, gy, gz) = dispatch_dims(n_out_s, 64);
8076 pass.dispatch_workgroups(gx, gy, gz);
8077 }
8078 Step::GatherAxis { params } => {
8079 let total_s = scale(params.total);
8080 if total_s == 0 {
8081 continue;
8082 }
8083 let gk = gather_axis_kernel(&dev.device);
8084 pass.set_pipeline(&gk.pipeline);
8085 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8086 let (gx, gy, gz) = dispatch_dims(total_s, 64);
8087 pass.dispatch_workgroups(gx, gy, gz);
8088 }
8089 Step::Attention { params, .. } => {
8090 let seq_q_s = scale(params.seq_q);
8094 if seq_q_s == 0 {
8095 continue;
8096 }
8097 let ak = attention_kernel(&dev.device);
8098 pass.set_pipeline(&ak.pipeline);
8099 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8100 let total = params.batch * params.heads * seq_q_s;
8101 let (gx, gy, gz) = dispatch_dims(total, 64);
8102 pass.dispatch_workgroups(gx, gy, gz);
8103 }
8104 Step::AttentionBackward { params, .. } => {
8105 let axis = if params.wrt == 0 {
8106 params.seq_q
8107 } else {
8108 params.seq_k
8109 };
8110 let axis_s = scale(axis);
8111 if axis_s == 0 {
8112 continue;
8113 }
8114 let ak = attention_bwd_kernel(&dev.device);
8115 pass.set_pipeline(&ak.pipeline);
8116 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8117 let total = params.batch * params.heads * axis_s;
8118 let (gx, gy, gz) = dispatch_dims(total, 64);
8119 pass.dispatch_workgroups(gx, gy, gz);
8120 }
8121 Step::Rope { params } => {
8122 let s_active = scale(params.seq);
8125 let total_s = params.batch * s_active * params.last_dim;
8126 if total_s == 0 {
8127 continue;
8128 }
8129 let rk = rope_kernel(&dev.device);
8130 pass.set_pipeline(&rk.pipeline);
8131 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8132 let (gx, gy, gz) = dispatch_dims(total_s, 64);
8133 pass.dispatch_workgroups(gx, gy, gz);
8134 }
8135 Step::Expand { params, .. } => {
8136 let total_s = if params.bucket_outermost == 1 && params.out_dim_0 > 0 {
8137 let scaled_d0 = scale(params.out_dim_0);
8138 let inner = params.out_total / params.out_dim_0;
8139 scaled_d0 * inner
8140 } else {
8141 params.out_total
8142 };
8143 if total_s == 0 {
8144 continue;
8145 }
8146 let ek = expand_kernel(&dev.device);
8147 pass.set_pipeline(&ek.pipeline);
8148 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8149 let (gx, gy, gz) = dispatch_dims(total_s, 64);
8150 pass.dispatch_workgroups(gx, gy, gz);
8151 }
8152 Step::Argmax { params } => {
8153 let outer_s = scale(params.outer);
8154 if outer_s == 0 {
8155 continue;
8156 }
8157 let amk = argmax_kernel(&dev.device);
8158 pass.set_pipeline(&amk.pipeline);
8159 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8160 let (gx, gy, gz) = dispatch_dims(outer_s, 64);
8161 pass.dispatch_workgroups(gx, gy, gz);
8162 }
8163 Step::Pool2d { params } => {
8164 let n_s = scale(params.n);
8165 if n_s == 0 {
8166 continue;
8167 }
8168 let pk = pool2d_kernel(&dev.device);
8169 pass.set_pipeline(&pk.pipeline);
8170 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8171 let total = n_s * params.c * params.h_out * params.w_out;
8172 let (gx, gy, gz) = dispatch_dims(total, 64);
8173 pass.dispatch_workgroups(gx, gy, gz);
8174 }
8175 Step::Conv2d { params } => {
8176 let n_s = scale(params.n);
8177 if n_s == 0 {
8178 continue;
8179 }
8180 let ck2 = conv2d_kernel(&dev.device);
8181 pass.set_pipeline(&ck2.pipeline);
8182 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8183 let spatial = params.h_out * params.w_out;
8186 let sp_tiles = spatial.div_ceil(CONV2D_TILE);
8187 let total = n_s * params.c_out * sp_tiles;
8188 let (gx, gy, gz) = dispatch_dims(total, 64);
8189 pass.dispatch_workgroups(gx, gy, gz);
8190 }
8191 Step::Pool1d { params } => {
8192 let n_s = scale(params.n);
8193 if n_s == 0 {
8194 continue;
8195 }
8196 let pk = pool1d_kernel(&dev.device);
8197 pass.set_pipeline(&pk.pipeline);
8198 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8199 let total = n_s * params.c * params.l_out;
8200 let (gx, gy, gz) = dispatch_dims(total, 64);
8201 pass.dispatch_workgroups(gx, gy, gz);
8202 }
8203 Step::Pool3d { params } => {
8204 let n_s = scale(params.n);
8205 if n_s == 0 {
8206 continue;
8207 }
8208 let pk = pool3d_kernel(&dev.device);
8209 pass.set_pipeline(&pk.pipeline);
8210 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8211 let total = n_s * params.c * params.d_out * params.h_out * params.w_out;
8212 let (gx, gy, gz) = dispatch_dims(total, 64);
8213 pass.dispatch_workgroups(gx, gy, gz);
8214 }
8215 Step::Conv1d { params } => {
8216 let n_s = scale(params.n);
8217 if n_s == 0 {
8218 continue;
8219 }
8220 let ck = conv1d_kernel(&dev.device);
8221 pass.set_pipeline(&ck.pipeline);
8222 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8223 let total = n_s * params.c_out * params.l_out;
8224 let (gx, gy, gz) = dispatch_dims(total, 64);
8225 pass.dispatch_workgroups(gx, gy, gz);
8226 }
8227 Step::Conv3d { params } => {
8228 let n_s = scale(params.n);
8229 if n_s == 0 {
8230 continue;
8231 }
8232 let ck = conv3d_kernel(&dev.device);
8233 pass.set_pipeline(&ck.pipeline);
8234 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8235 let total =
8236 n_s * params.c_out * params.d_out * params.h_out * params.w_out;
8237 let (gx, gy, gz) = dispatch_dims(total, 64);
8238 pass.dispatch_workgroups(gx, gy, gz);
8239 }
8240 Step::ScatterAdd { params } => {
8241 let sk = scatter_add_kernel(&dev.device);
8242 pass.set_pipeline(&sk.pipeline);
8243 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8244 if params.op == 0 {
8250 let (gx, gy, gz) = dispatch_dims(params.out_total, 64);
8251 pass.dispatch_workgroups(gx, gy, gz);
8252 } else {
8253 pass.dispatch_workgroups(1, 1, 1);
8254 }
8255 }
8256 Step::TopK { params } => {
8257 let outer_s = scale(params.outer);
8258 if outer_s == 0 {
8259 continue;
8260 }
8261 let tk = topk_kernel(&dev.device);
8262 pass.set_pipeline(&tk.pipeline);
8263 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8264 let (gx, gy, gz) = dispatch_dims(outer_s, 64);
8265 pass.dispatch_workgroups(gx, gy, gz);
8266 }
8267 Step::WelchPeaksGpu { params } => {
8268 let batch_s = scale(params.welch_batch);
8269 if batch_s == 0 {
8270 continue;
8271 }
8272 let wk = welch_peaks_gpu_kernel(&dev.device);
8273 pass.set_pipeline(&wk.pipeline);
8274 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8275 let (gx, gy, gz) = dispatch_dims(batch_s, 64);
8276 pass.dispatch_workgroups(gx, gy, gz);
8277 }
8278 Step::UmapKnn { params } => {
8279 let n_s = scale(params.n);
8280 if n_s == 0 {
8281 continue;
8282 }
8283 let uk = umap_knn_kernel(&dev.device);
8284 pass.set_pipeline(&uk.pipeline);
8285 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8286 let (gx, gy, gz) = dispatch_dims(n_s, 64);
8287 pass.dispatch_workgroups(gx, gy, gz);
8288 }
8289 Step::GroupedMatmul { params } => {
8290 let m_s = scale(params.m);
8291 if m_s == 0 {
8292 continue;
8293 }
8294 let gk = grouped_matmul_kernel(&dev.device);
8295 pass.set_pipeline(&gk.pipeline);
8296 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8297 pass.dispatch_workgroups(params.n.div_ceil(8), m_s.div_ceil(8), 1);
8298 }
8299 Step::Sample { params } => {
8300 let outer_s = scale(params.outer);
8301 if outer_s == 0 {
8302 continue;
8303 }
8304 let sk = sample_kernel(&dev.device);
8305 pass.set_pipeline(&sk.pipeline);
8306 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8307 let (gx, gy, gz) = dispatch_dims(outer_s, 64);
8308 pass.dispatch_workgroups(gx, gy, gz);
8309 }
8310 Step::SelectiveScan { params } => {
8311 let ssk = selective_scan_kernel(&dev.device);
8316 pass.set_pipeline(&ssk.pipeline);
8317 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8318 let total = params.batch * params.hidden;
8319 let (gx, gy, gz) = dispatch_dims(total, 64);
8320 pass.dispatch_workgroups(gx, gy, gz);
8321 }
8322 Step::Mamba2 { params } => {
8323 let mk = mamba2_kernel(&dev.device);
8324 pass.set_pipeline(&mk.pipeline);
8325 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8326 let total = params.batch * params.heads * params.head_dim;
8327 let (gx, gy, gz) = dispatch_dims(total, 64);
8328 pass.dispatch_workgroups(gx, gy, gz);
8329 }
8330 Step::Gru { params } => {
8331 let gk = gru_kernel(&dev.device);
8333 pass.set_pipeline(&gk.pipeline);
8334 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8335 pass.dispatch_workgroups(params.batch, 1, 1);
8336 }
8337 Step::Rnn { params } => {
8338 let rk = rnn_kernel(&dev.device);
8339 pass.set_pipeline(&rk.pipeline);
8340 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8341 pass.dispatch_workgroups(params.batch, 1, 1);
8342 }
8343 Step::DequantMatmul { params } => {
8344 let m_s = scale(params.m);
8345 if m_s == 0 {
8346 continue;
8347 }
8348 let dk = dequant_matmul_kernel(&dev.device);
8349 pass.set_pipeline(&dk.pipeline);
8350 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8351 pass.dispatch_workgroups(params.n.div_ceil(8), m_s.div_ceil(8), 1);
8352 }
8353 Step::FusedResidualLn { params } => {
8354 let outer_s = scale(params.outer);
8355 if outer_s == 0 {
8356 continue;
8357 }
8358 let frk = fused_residual_ln_kernel(&dev.device);
8359 pass.set_pipeline(&frk.pipeline);
8360 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8361 let (gx, gy, gz) = dispatch_dims(outer_s, 64);
8362 pass.dispatch_workgroups(gx, gy, gz);
8363 }
8364 Step::FusedResidualLnTee { params } => {
8365 let outer_s = scale(params.outer);
8366 if outer_s == 0 {
8367 continue;
8368 }
8369 let frtk = fused_residual_ln_tee_kernel(&dev.device);
8370 pass.set_pipeline(&frtk.pipeline);
8371 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8372 let (gx, gy, gz) = dispatch_dims(outer_s, 64);
8373 pass.dispatch_workgroups(gx, gy, gz);
8374 }
8375 Step::FusedResidualRmsNorm { params } => {
8376 let outer_s = scale(params.outer);
8377 if outer_s == 0 {
8378 continue;
8379 }
8380 let frk = fused_residual_rms_norm_kernel(&dev.device);
8381 pass.set_pipeline(&frk.pipeline);
8382 pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
8383 let (gx, gy, gz) = dispatch_dims(outer_s, 64);
8384 pass.dispatch_workgroups(gx, gy, gz);
8385 }
8386 Step::MatmulQkv { params, kind } => {
8387 let m_s = scale(params.m);
8388 if m_s == 0 {
8389 continue;
8390 }
8391 let qkv_coop_wide = matches!(kind, MatmulQkvKind::CoopF16Vk)
8392 && crate::coop_f16_vk::use_wide_matmul(
8393 params.b_off,
8394 params.n,
8395 &self.coop_f16_b_param,
8396 &self.coop_f16_vk_wide_b,
8397 );
8398 pass.set_bind_group(
8399 0,
8400 coop_f16_vk_bind_group(self, gpu_bi, qkv_coop_wide),
8401 &[],
8402 );
8403 match kind {
8404 MatmulQkvKind::CoopF16Vk => {
8405 if qkv_coop_wide {
8406 pass.set_pipeline(&matmul_qkv_kernel(&dev.device).pipeline);
8407 pass.dispatch_workgroups(
8408 params.n.div_ceil(32),
8409 m_s.div_ceil(32),
8410 1,
8411 );
8412 } else {
8413 let n_eff = scale(params.n);
8414 let mqk = matmul_qkv_coop_f16_vk_active_kernel(
8415 &dev.device,
8416 n_eff,
8417 )
8418 .expect("coop f16 matmul_qkv kernel missing");
8419 pass.set_pipeline(&mqk.pipeline);
8420 pass.dispatch_workgroups(
8421 m_s.div_ceil(16),
8422 params.n.div_ceil(16),
8423 1,
8424 );
8425 }
8426 }
8427 MatmulQkvKind::CoopF32 => {
8428 pass.set_pipeline(
8429 &matmul_qkv_coop_f32_kernel(&dev.device)
8430 .expect("coop matmul_qkv kernel missing")
8431 .pipeline,
8432 );
8433 pass.dispatch_workgroups(
8434 params.n.div_ceil(32),
8435 m_s.div_ceil(32),
8436 1,
8437 );
8438 }
8439 MatmulQkvKind::F32 => {
8440 pass.set_pipeline(&matmul_qkv_kernel(&dev.device).pipeline);
8441 pass.dispatch_workgroups(
8442 params.n.div_ceil(32),
8443 m_s.div_ceil(32),
8444 1,
8445 );
8446 }
8447 }
8448 }
8449 Step::GatherSplit { .. }
8450 | Step::DequantMatmulGguf { .. }
8451 | Step::DequantGroupedMatmulGguf { .. }
8452 | Step::GatedDeltaNet { .. }
8453 | Step::Lstm { .. }
8454 | Step::ConvTranspose2d { .. }
8455 | Step::GroupNormHost { .. }
8456 | Step::LayerNorm2dHost { .. }
8457 | Step::ResizeNearest2xHost { .. }
8458 | Step::ReverseHost { .. }
8459 | Step::ArgReduceHost { .. }
8460 | Step::GruHost { .. }
8461 | Step::RnnHost { .. }
8462 | Step::Llada2GroupLimitedGate { .. }
8463 | Step::UmapKnnHost { .. }
8464 | Step::MsDeformAttnHost { .. }
8465 | Step::FftHost { .. }
8466 | Step::Im2ColHost { .. }
8467 | Step::RngNormalHost { .. }
8468 | Step::RngUniformHost { .. }
8469 | Step::WelchPeaksHost { .. }
8470 | Step::LogMelHost { .. }
8471 | Step::LogMelBackwardHost { .. } => {}
8472 #[cfg(feature = "splat")]
8473 Step::GaussianSplatRender { .. }
8474 | Step::GaussianSplatRenderBackward { .. }
8475 | Step::GaussianSplatPrepare { .. }
8476 | Step::GaussianSplatRasterize { .. } => {}
8477 }
8478 if !matches!(step, Step::FftGpu { .. }) {
8479 gpu_bi += 1;
8480 }
8481 step_i += 1;
8482 pass_dispatched = true;
8483 }
8484 }
8485 let needs_f16_drain = step_i < self.schedule.len()
8486 && !step_runs_on_host(&self.schedule[step_i])
8487 && step_i > 0
8488 && step_needs_pass_flush(&self.schedule[step_i], &self.schedule[step_i - 1]);
8489 let gpu_schedule_done = step_i >= self.schedule.len();
8490 let skip_readback = rlx_ir::env::flag("RLX_BENCH_DISPATCH_ONLY") || self.dispatch_only;
8491 let defer_tail = gpu_schedule_done && self.schedule.iter().any(step_is_tail_host);
8492 let mut fused_readback: Option<(
8493 ReadbackLayout,
8494 std::sync::mpsc::Receiver<Result<(), wgpu::BufferAsyncError>>,
8495 Vec<usize>,
8496 )> = None;
8497 if gpu_schedule_done && !skip_readback && !defer_tail {
8498 if !self.gpu_handle_feeds.is_empty() {
8499 self.propagate_gpu_handle_feeds_on_gpu(dev, &mut enc);
8500 }
8501 let plan = self.readback_plan();
8502 let out_ids_all: Vec<_> = self.graph.outputs.clone();
8503 let out_ids: Vec<_> = plan.iter().map(|&i| out_ids_all[i]).collect();
8504 let layout = ReadbackLayout::for_nodes(&self.arena, &out_ids);
8505 if use_tiny_readback(&layout, out_ids.len()) && plan == vec![0] {
8506 if self.tiny_readback.is_none() {
8507 self.tiny_readback = Some(TinyReadbackStaging::new(&dev.device));
8508 }
8509 let tiny = self.tiny_readback.as_ref().expect("tiny readback");
8510 encode_readback_copies(&mut enc, &self.arena, tiny.buffer(), &out_ids, &layout);
8511 let map_rx = schedule_readback_map(&mut enc, tiny.buffer(), &layout);
8512 let sub = dev.queue.submit(std::iter::once(enc.finish()));
8513 wait_readback_map(&dev.device, sub, &map_rx, layout.total_bytes);
8514 map_rx.recv().unwrap().unwrap();
8515 return self.pack_readback_outputs(
8516 &plan,
8517 vec![decode_tiny_mapped_f32(tiny.buffer(), layout.total_bytes)],
8518 );
8519 }
8520 ReadbackStaging::prepare(
8521 &dev.device,
8522 &mut self.readback_staging,
8523 layout.total_bytes,
8524 );
8525 if let Some(staging) = self.readback_staging.as_ref() {
8526 encode_readback_copies(
8527 &mut enc,
8528 &self.arena,
8529 staging.buffer(),
8530 &out_ids,
8531 &layout,
8532 );
8533 let map_rx = schedule_readback_map(&mut enc, staging.buffer(), &layout);
8534 fused_readback = Some((layout, map_rx, plan));
8535 }
8536 }
8537 let main_submission = dev.queue.submit(std::iter::once(enc.finish()));
8538 if defer_tail {
8539 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
8540 self.run_tail_host_audio_ops(dev);
8541 if !skip_readback {
8542 let mut rb_enc =
8543 dev.device
8544 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
8545 label: Some("rlx-wgpu readback after tail-host"),
8546 });
8547 if !self.gpu_handle_feeds.is_empty() {
8548 self.propagate_gpu_handle_feeds_on_gpu(dev, &mut rb_enc);
8549 }
8550 let plan = self.readback_plan();
8551 let out_ids_all: Vec<_> = self.graph.outputs.clone();
8552 let out_ids: Vec<_> = plan.iter().map(|&i| out_ids_all[i]).collect();
8553 let layout = ReadbackLayout::for_nodes(&self.arena, &out_ids);
8554 if use_tiny_readback(&layout, out_ids.len()) && plan == vec![0] {
8555 if self.tiny_readback.is_none() {
8556 self.tiny_readback = Some(TinyReadbackStaging::new(&dev.device));
8557 }
8558 let tiny = self.tiny_readback.as_ref().expect("tiny readback");
8559 encode_readback_copies(
8560 &mut rb_enc,
8561 &self.arena,
8562 tiny.buffer(),
8563 &out_ids,
8564 &layout,
8565 );
8566 let map_rx = schedule_readback_map(&mut rb_enc, tiny.buffer(), &layout);
8567 let sub = dev.queue.submit(std::iter::once(rb_enc.finish()));
8568 wait_readback_map(&dev.device, sub, &map_rx, layout.total_bytes);
8569 map_rx.recv().unwrap().unwrap();
8570 return self.pack_readback_outputs(
8571 &plan,
8572 vec![decode_tiny_mapped_f32(tiny.buffer(), layout.total_bytes)],
8573 );
8574 }
8575 ReadbackStaging::prepare(
8576 &dev.device,
8577 &mut self.readback_staging,
8578 layout.total_bytes,
8579 );
8580 if let Some(staging) = self.readback_staging.as_ref() {
8581 encode_readback_copies(
8582 &mut rb_enc,
8583 &self.arena,
8584 staging.buffer(),
8585 &out_ids,
8586 &layout,
8587 );
8588 let map_rx = schedule_readback_map(&mut rb_enc, staging.buffer(), &layout);
8589 let sub = dev.queue.submit(std::iter::once(rb_enc.finish()));
8590 wait_readback_map(&dev.device, sub, &map_rx, layout.total_bytes);
8591 map_rx.recv().unwrap().unwrap();
8592 self.dump_node_stats_if_requested(dev);
8593 let partial = decode_mapped_readback_f32(staging.buffer(), &layout);
8594 return self.pack_readback_outputs(&plan, partial);
8595 }
8596 }
8597 }
8598 if needs_f16_drain {
8599 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
8600 }
8601 let need_host_sync =
8602 step_i < self.schedule.len() && step_runs_on_host(&self.schedule[step_i]);
8603 if need_host_sync {
8604 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
8605 }
8606 if gpu_schedule_done {
8607 if skip_readback || defer_tail {
8608 return self
8609 .graph
8610 .outputs
8611 .iter()
8612 .map(|&id| {
8613 let n = self.graph.node(id).shape.num_elements().unwrap_or(0);
8614 vec![0.0; n]
8615 })
8616 .collect();
8617 }
8618 if let (Some((layout, map_rx, plan)), Some(staging)) =
8619 (fused_readback, self.readback_staging.as_ref())
8620 {
8621 wait_readback_map(&dev.device, main_submission, &map_rx, layout.total_bytes);
8622 map_rx.recv().unwrap().unwrap();
8623 self.dump_node_stats_if_requested(dev);
8624 let partial = decode_mapped_readback_f32(staging.buffer(), &layout);
8625 return self.pack_readback_outputs(&plan, partial);
8626 }
8627 break;
8628 }
8629 match &self.schedule[step_i] {
8630 Step::BufferCopy {
8631 src_byte_off,
8632 dst_byte_off,
8633 bytes,
8634 } => {
8635 let src = *src_byte_off;
8638 let dst = *dst_byte_off;
8639 let nbytes = *bytes as u64;
8640 let elems = (nbytes / 4).max(1) as u32;
8641 let lo = src.min(dst);
8642 let hi = src.saturating_add(nbytes).max(dst.saturating_add(nbytes));
8643 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
8644 if hi.saturating_sub(lo) > max_binding {
8650 let bytes_host = self.arena.read_bytes_range(
8651 &dev.device,
8652 &dev.queue,
8653 src as usize,
8654 nbytes as usize,
8655 );
8656 self.arena
8657 .write_bytes_range(&dev.queue, dst as usize, &bytes_host);
8658 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
8659 step_i += 1;
8660 continue;
8661 }
8662 let mut base = (lo / 256) * 256;
8663 let span = hi.saturating_sub(base).max(1);
8669 let mut size = span.div_ceil(256) * 256;
8670 size = size.max(256).min(max_binding);
8671 if base.saturating_add(size) > self.arena.size as u64 {
8672 base = (self.arena.size as u64).saturating_sub(size);
8673 base = (base / 256) * 256;
8674 }
8675 let p = CopyParams {
8676 n: elems,
8677 in_off: (src.saturating_sub(base) / 4) as u32,
8678 out_off: (dst.saturating_sub(base) / 4) as u32,
8679 _p0: 0,
8680 _p1: 0,
8681 _p2: 0,
8682 _p3: 0,
8683 _p4: 0,
8684 };
8685 let ck = copy_kernel(&dev.device);
8686 let u = dev.device.create_buffer(&wgpu::BufferDescriptor {
8687 label: Some("rlx-wgpu arena_copy uniform"),
8688 size: std::mem::size_of::<CopyParams>() as u64,
8689 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
8690 mapped_at_creation: false,
8691 });
8692 dev.queue.write_buffer(&u, 0, bytemuck::bytes_of(&p));
8693 let bg =
8694 bind_two_buf0_window(&dev.device, ck, &self.arena.buffer, base, size, &u);
8695 let mut enc =
8696 dev.device
8697 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
8698 label: Some("rlx-wgpu arena_copy"),
8699 });
8700 {
8701 let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
8702 label: Some("rlx-wgpu arena_copy pass"),
8703 ..Default::default()
8704 });
8705 pass.set_pipeline(&ck.pipeline);
8706 pass.set_bind_group(0, &bg, &[]);
8707 let (gx, gy, gz) = dispatch_dims(elems, 64);
8708 pass.dispatch_workgroups(gx, gy, gz);
8709 }
8710 dev.queue.submit(std::iter::once(enc.finish()));
8711 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
8712 }
8713 Step::GatherSplit {
8714 n_out,
8715 n_idx,
8716 dim,
8717 vocab,
8718 table_byte_off,
8719 idx_byte_off,
8720 out_byte_off,
8721 } => {
8722 run_gather_split(
8723 &self.arena,
8724 &dev.device,
8725 &dev.queue,
8726 *n_out,
8727 *n_idx,
8728 *dim,
8729 *vocab,
8730 *table_byte_off as usize,
8731 *idx_byte_off as usize,
8732 *out_byte_off as usize,
8733 );
8734 }
8735 Step::DequantMatmulGguf {
8736 m,
8737 k,
8738 n,
8739 scheme_id,
8740 x_byte_off,
8741 w_byte_off,
8742 out_byte_off,
8743 } => {
8744 if *m == 1 && crate::gguf_gpu::gemv_supports_scheme(*scheme_id) {
8745 crate::gguf_gpu::run_dequant_matmul_gguf_gemv(
8749 &self.arena,
8750 &dev.device,
8751 &dev.queue,
8752 *k as usize,
8753 *n as usize,
8754 *scheme_id,
8755 *x_byte_off as usize,
8756 *w_byte_off as usize,
8757 *out_byte_off as usize,
8758 );
8759 } else if self.dequant_scratch_off > 0 {
8760 crate::gguf_gpu::run_dequant_matmul_gguf_gpu(
8761 &self.arena,
8762 &dev.device,
8763 &dev.queue,
8764 *m as usize,
8765 *k as usize,
8766 *n as usize,
8767 *scheme_id,
8768 *x_byte_off as usize,
8769 *w_byte_off as usize,
8770 self.dequant_scratch_off,
8771 *out_byte_off as usize,
8772 );
8773 } else {
8774 crate::gguf_host::run_dequant_matmul_gguf(
8775 &self.arena,
8776 &dev.device,
8777 &dev.queue,
8778 *m as usize,
8779 *k as usize,
8780 *n as usize,
8781 *scheme_id,
8782 *x_byte_off as usize,
8783 *w_byte_off as usize,
8784 *out_byte_off as usize,
8785 );
8786 }
8787 }
8788 Step::DequantGroupedMatmulGguf {
8789 m,
8790 k,
8791 n,
8792 num_experts,
8793 scheme_id,
8794 x_byte_off,
8795 w_byte_off,
8796 idx_byte_off,
8797 out_byte_off,
8798 } => {
8799 if self.dequant_scratch_off > 0 {
8800 crate::gguf_gpu::run_dequant_grouped_matmul_gguf_gpu(
8801 &self.arena,
8802 &dev.device,
8803 &dev.queue,
8804 *m as usize,
8805 *k as usize,
8806 *n as usize,
8807 *num_experts as usize,
8808 *scheme_id,
8809 *x_byte_off as usize,
8810 *w_byte_off as usize,
8811 *idx_byte_off as usize,
8812 self.dequant_scratch_off,
8813 *out_byte_off as usize,
8814 );
8815 } else {
8816 crate::gguf_host::run_dequant_grouped_matmul_gguf(
8817 &self.arena,
8818 &dev.device,
8819 &dev.queue,
8820 *m as usize,
8821 *k as usize,
8822 *n as usize,
8823 *num_experts as usize,
8824 *scheme_id,
8825 *x_byte_off as usize,
8826 *w_byte_off as usize,
8827 *idx_byte_off as usize,
8828 *out_byte_off as usize,
8829 );
8830 }
8831 }
8832 Step::GatedDeltaNet {
8833 q_byte_off,
8834 k_byte_off,
8835 v_byte_off,
8836 g_byte_off,
8837 beta_byte_off,
8838 state_byte_off,
8839 dst_byte_off,
8840 batch,
8841 seq,
8842 heads,
8843 state_size,
8844 use_carry,
8845 } => {
8846 crate::gdn_host::run_gated_delta_net(
8847 &self.arena,
8848 &dev.device,
8849 &dev.queue,
8850 *q_byte_off as usize,
8851 *k_byte_off as usize,
8852 *v_byte_off as usize,
8853 *g_byte_off as usize,
8854 *beta_byte_off as usize,
8855 *state_byte_off as usize,
8856 *dst_byte_off as usize,
8857 *batch as usize,
8858 *seq as usize,
8859 *heads as usize,
8860 *state_size as usize,
8861 *use_carry,
8862 );
8863 }
8864 Step::Lstm {
8865 x_byte_off,
8866 w_ih_byte_off,
8867 w_hh_byte_off,
8868 bias_byte_off,
8869 h0_byte_off,
8870 c0_byte_off,
8871 dst_byte_off,
8872 batch,
8873 seq,
8874 input_size,
8875 hidden,
8876 num_layers,
8877 bidirectional,
8878 carry,
8879 } => {
8880 crate::lstm_host::run_lstm(
8881 &self.arena,
8882 &dev.device,
8883 &dev.queue,
8884 *x_byte_off as usize,
8885 *w_ih_byte_off as usize,
8886 *w_hh_byte_off as usize,
8887 *bias_byte_off as usize,
8888 *h0_byte_off as usize,
8889 *c0_byte_off as usize,
8890 *dst_byte_off as usize,
8891 *batch as usize,
8892 *seq as usize,
8893 *input_size as usize,
8894 *hidden as usize,
8895 *num_layers as usize,
8896 *bidirectional,
8897 *carry,
8898 );
8899 }
8900 Step::ConvTranspose2d {
8901 src_byte_off,
8902 weight_byte_off,
8903 dst_byte_off,
8904 n,
8905 c_in,
8906 h,
8907 w_in,
8908 c_out,
8909 h_out,
8910 w_out,
8911 kh,
8912 kw,
8913 sh,
8914 sw,
8915 ph,
8916 pw,
8917 dh,
8918 dw,
8919 groups,
8920 } => {
8921 crate::conv_transpose2d_host::run_conv_transpose2d(
8922 &self.arena,
8923 &dev.device,
8924 &dev.queue,
8925 *src_byte_off as usize,
8926 *weight_byte_off as usize,
8927 *dst_byte_off as usize,
8928 *n as usize,
8929 *c_in as usize,
8930 *h as usize,
8931 *w_in as usize,
8932 *c_out as usize,
8933 *h_out as usize,
8934 *w_out as usize,
8935 *kh as usize,
8936 *kw as usize,
8937 *sh as usize,
8938 *sw as usize,
8939 *ph as usize,
8940 *pw as usize,
8941 *dh as usize,
8942 *dw as usize,
8943 *groups as usize,
8944 );
8945 }
8946 Step::GroupNormHost {
8947 src_byte_off,
8948 gamma_byte_off,
8949 beta_byte_off,
8950 dst_byte_off,
8951 n,
8952 c,
8953 h,
8954 w,
8955 num_groups,
8956 eps,
8957 } => {
8958 crate::vision_host::run_group_norm(
8959 &self.arena,
8960 &dev.device,
8961 &dev.queue,
8962 *src_byte_off as usize,
8963 *gamma_byte_off as usize,
8964 *beta_byte_off as usize,
8965 *dst_byte_off as usize,
8966 *n as usize,
8967 *c as usize,
8968 *h as usize,
8969 *w as usize,
8970 *num_groups as usize,
8971 *eps,
8972 );
8973 }
8974 Step::LayerNorm2dHost {
8975 src_byte_off,
8976 gamma_byte_off,
8977 beta_byte_off,
8978 dst_byte_off,
8979 n,
8980 c,
8981 h,
8982 w,
8983 eps,
8984 } => {
8985 crate::vision_host::run_layer_norm2d(
8986 &self.arena,
8987 &dev.device,
8988 &dev.queue,
8989 *src_byte_off as usize,
8990 *gamma_byte_off as usize,
8991 *beta_byte_off as usize,
8992 *dst_byte_off as usize,
8993 *n as usize,
8994 *c as usize,
8995 *h as usize,
8996 *w as usize,
8997 *eps,
8998 );
8999 }
9000 Step::ResizeNearest2xHost {
9001 src_byte_off,
9002 dst_byte_off,
9003 n,
9004 c,
9005 h,
9006 w,
9007 } => {
9008 crate::vision_host::run_resize_nearest_2x(
9009 &self.arena,
9010 &dev.device,
9011 &dev.queue,
9012 *src_byte_off as usize,
9013 *dst_byte_off as usize,
9014 *n as usize,
9015 *c as usize,
9016 *h as usize,
9017 *w as usize,
9018 );
9019 }
9020 Step::ReverseHost {
9021 src_byte_off,
9022 dst_byte_off,
9023 dims,
9024 rev_mask,
9025 elem_bytes,
9026 } => {
9027 crate::vision_host::run_reverse(
9028 &self.arena,
9029 &dev.device,
9030 &dev.queue,
9031 *src_byte_off as usize,
9032 *dst_byte_off as usize,
9033 dims,
9034 rev_mask,
9035 *elem_bytes as usize,
9036 );
9037 }
9038 Step::ArgReduceHost {
9039 src_byte_off,
9040 dst_byte_off,
9041 outer,
9042 reduced,
9043 inner,
9044 is_max,
9045 } => {
9046 crate::vision_host::run_argreduce(
9047 &self.arena,
9048 &dev.device,
9049 &dev.queue,
9050 *src_byte_off as usize,
9051 *dst_byte_off as usize,
9052 *outer as usize,
9053 *reduced as usize,
9054 *inner as usize,
9055 *is_max,
9056 );
9057 }
9058 Step::GruHost {
9059 x,
9060 w_ih,
9061 w_hh,
9062 b_ih,
9063 b_hh,
9064 h0,
9065 dst,
9066 batch,
9067 seq,
9068 input_size,
9069 hidden,
9070 num_layers,
9071 bidirectional,
9072 carry,
9073 } => {
9074 crate::vision_host::run_gru(
9075 &self.arena,
9076 &dev.device,
9077 &dev.queue,
9078 *x as usize,
9079 *w_ih as usize,
9080 *w_hh as usize,
9081 *b_ih as usize,
9082 *b_hh as usize,
9083 *h0 as usize,
9084 *dst as usize,
9085 *batch as usize,
9086 *seq as usize,
9087 *input_size as usize,
9088 *hidden as usize,
9089 *num_layers as usize,
9090 *bidirectional,
9091 *carry,
9092 );
9093 }
9094 Step::RnnHost {
9095 x,
9096 w_ih,
9097 w_hh,
9098 bias,
9099 h0,
9100 dst,
9101 batch,
9102 seq,
9103 input_size,
9104 hidden,
9105 num_layers,
9106 bidirectional,
9107 carry,
9108 relu,
9109 } => {
9110 crate::vision_host::run_rnn(
9111 &self.arena,
9112 &dev.device,
9113 &dev.queue,
9114 *x as usize,
9115 *w_ih as usize,
9116 *w_hh as usize,
9117 *bias as usize,
9118 *h0 as usize,
9119 *dst as usize,
9120 *batch as usize,
9121 *seq as usize,
9122 *input_size as usize,
9123 *hidden as usize,
9124 *num_layers as usize,
9125 *bidirectional,
9126 *carry,
9127 *relu,
9128 );
9129 }
9130 Step::Llada2GroupLimitedGate {
9131 sig_byte_off,
9132 route_byte_off,
9133 out_byte_off,
9134 n_elems,
9135 attrs,
9136 } => {
9137 crate::llada2_gate_host::run_llada2_group_limited_gate(
9138 &self.arena,
9139 &dev.device,
9140 &dev.queue,
9141 *sig_byte_off as usize,
9142 *route_byte_off as usize,
9143 *out_byte_off as usize,
9144 *n_elems as usize,
9145 attrs,
9146 );
9147 }
9148 Step::UmapKnnHost {
9149 pairwise_byte_off,
9150 out_byte_off,
9151 n,
9152 k,
9153 } => {
9154 crate::umap_knn_host::run_umap_knn(
9155 &self.arena,
9156 &dev.device,
9157 &dev.queue,
9158 *pairwise_byte_off as usize,
9159 *out_byte_off as usize,
9160 *n as usize,
9161 *k as usize,
9162 );
9163 }
9164 Step::MsDeformAttnHost {
9165 in_offs,
9166 out_byte_off,
9167 out_bytes,
9168 attrs,
9169 } => {
9170 crate::ms_deform_attn::run_ms_deform_attn(
9171 &self.arena,
9172 &dev.device,
9173 &dev.queue,
9174 in_offs,
9175 *out_byte_off as usize,
9176 *out_bytes as usize,
9177 attrs,
9178 );
9179 }
9180 Step::FftHost {
9181 src_byte_off,
9182 dst_byte_off,
9183 outer,
9184 n_complex,
9185 inverse,
9186 norm_tag,
9187 dtype_tag,
9188 } => {
9189 crate::fft_host::run_fft1d(
9190 &self.arena,
9191 &dev.device,
9192 &dev.queue,
9193 *src_byte_off as usize,
9194 *dst_byte_off as usize,
9195 *outer as usize,
9196 *n_complex as usize,
9197 *inverse,
9198 *norm_tag,
9199 fft_dtype_from_tag(*dtype_tag),
9200 );
9201 }
9202 Step::WelchPeaksHost { .. }
9203 | Step::LogMelHost { .. }
9204 | Step::LogMelBackwardHost { .. } => {
9205 unreachable!("tail-host audio ops run after GPU wait")
9206 }
9207 Step::Im2ColHost {
9208 x_byte_off,
9209 col_byte_off,
9210 n,
9211 c_in,
9212 h,
9213 w,
9214 h_out,
9215 w_out,
9216 kh,
9217 kw,
9218 sh,
9219 sw,
9220 ph,
9221 pw,
9222 dh,
9223 dw_dil,
9224 } => {
9225 crate::im2col_host::run_im2col(
9226 &self.arena,
9227 &dev.device,
9228 &dev.queue,
9229 *x_byte_off as usize,
9230 *col_byte_off as usize,
9231 *n,
9232 *c_in,
9233 *h,
9234 *w,
9235 *h_out,
9236 *w_out,
9237 *kh,
9238 *kw,
9239 *sh,
9240 *sw,
9241 *ph,
9242 *pw,
9243 *dh,
9244 *dw_dil,
9245 );
9246 }
9247 Step::RngNormalHost {
9248 dst_byte_off,
9249 len,
9250 mean,
9251 scale,
9252 key,
9253 op_seed,
9254 } => {
9255 let opts = *self.rng.read().expect("rng lock");
9256 crate::rng_host::run_rng_normal(
9257 &self.arena,
9258 &dev.queue,
9259 *dst_byte_off as usize,
9260 *len as usize,
9261 *mean,
9262 *scale,
9263 *key,
9264 *op_seed,
9265 opts,
9266 );
9267 }
9268 Step::RngUniformHost {
9269 dst_byte_off,
9270 len,
9271 low,
9272 high,
9273 key,
9274 op_seed,
9275 } => {
9276 let opts = *self.rng.read().expect("rng lock");
9277 crate::rng_host::run_rng_uniform(
9278 &self.arena,
9279 &dev.queue,
9280 *dst_byte_off as usize,
9281 *len as usize,
9282 *low,
9283 *high,
9284 *key,
9285 *op_seed,
9286 opts,
9287 );
9288 }
9289 #[cfg(feature = "splat")]
9290 Step::GaussianSplatRender {
9291 positions_byte_off,
9292 positions_len,
9293 scales_byte_off,
9294 scales_len,
9295 rotations_byte_off,
9296 rotations_len,
9297 opacities_byte_off,
9298 opacities_len,
9299 colors_byte_off,
9300 colors_len,
9301 sh_coeffs_byte_off,
9302 sh_coeffs_len,
9303 meta_byte_off,
9304 dst_byte_off,
9305 dst_len,
9306 width,
9307 height,
9308 tile_size,
9309 radius_scale,
9310 alpha_cutoff,
9311 max_splat_steps,
9312 transmittance_threshold,
9313 max_list_entries,
9314 } => {
9315 crate::splat::run_gaussian_splat_render(
9316 &self.arena,
9317 &dev.device,
9318 &dev.queue,
9319 *positions_byte_off as usize,
9320 *positions_len as usize,
9321 *scales_byte_off as usize,
9322 *scales_len as usize,
9323 *rotations_byte_off as usize,
9324 *rotations_len as usize,
9325 *opacities_byte_off as usize,
9326 *opacities_len as usize,
9327 *colors_byte_off as usize,
9328 *colors_len as usize,
9329 *sh_coeffs_byte_off as usize,
9330 *sh_coeffs_len as usize,
9331 *meta_byte_off as usize,
9332 *dst_byte_off as usize,
9333 *dst_len as usize,
9334 *width,
9335 *height,
9336 *tile_size,
9337 *radius_scale,
9338 *alpha_cutoff,
9339 *max_splat_steps,
9340 *transmittance_threshold,
9341 *max_list_entries,
9342 );
9343 }
9344 #[cfg(feature = "splat")]
9345 Step::GaussianSplatPrepare {
9346 positions_byte_off,
9347 positions_len,
9348 scales_byte_off,
9349 scales_len,
9350 rotations_byte_off,
9351 rotations_len,
9352 opacities_byte_off,
9353 opacities_len,
9354 colors_byte_off,
9355 colors_len,
9356 sh_coeffs_byte_off,
9357 sh_coeffs_len,
9358 meta_byte_off,
9359 meta_len,
9360 prep_byte_off,
9361 prep_len,
9362 width,
9363 height,
9364 tile_size,
9365 radius_scale,
9366 alpha_cutoff,
9367 max_splat_steps,
9368 transmittance_threshold,
9369 max_list_entries,
9370 } => {
9371 crate::splat::run_gaussian_splat_prepare(
9372 &self.arena,
9373 &dev.device,
9374 &dev.queue,
9375 *positions_byte_off as usize,
9376 *positions_len as usize,
9377 *scales_byte_off as usize,
9378 *scales_len as usize,
9379 *rotations_byte_off as usize,
9380 *rotations_len as usize,
9381 *opacities_byte_off as usize,
9382 *opacities_len as usize,
9383 *colors_byte_off as usize,
9384 *colors_len as usize,
9385 *sh_coeffs_byte_off as usize,
9386 *sh_coeffs_len as usize,
9387 *meta_byte_off as usize,
9388 *meta_len as usize,
9389 *prep_byte_off as usize,
9390 *prep_len as usize,
9391 *width,
9392 *height,
9393 *tile_size,
9394 *radius_scale,
9395 *alpha_cutoff,
9396 *max_splat_steps,
9397 *transmittance_threshold,
9398 *max_list_entries,
9399 );
9400 }
9401 #[cfg(feature = "splat")]
9402 Step::GaussianSplatRasterize {
9403 prep_byte_off,
9404 prep_len,
9405 meta_byte_off,
9406 meta_len,
9407 dst_byte_off,
9408 dst_len,
9409 count,
9410 width,
9411 height,
9412 tile_size,
9413 alpha_cutoff,
9414 max_splat_steps,
9415 transmittance_threshold,
9416 max_list_entries,
9417 } => {
9418 crate::splat::run_gaussian_splat_rasterize(
9419 &self.arena,
9420 &dev.device,
9421 &dev.queue,
9422 *prep_byte_off as usize,
9423 *prep_len as usize,
9424 *meta_byte_off as usize,
9425 *meta_len as usize,
9426 *dst_byte_off as usize,
9427 *dst_len as usize,
9428 *count as usize,
9429 *width,
9430 *height,
9431 *tile_size,
9432 *alpha_cutoff,
9433 *max_splat_steps,
9434 *transmittance_threshold,
9435 *max_list_entries,
9436 );
9437 }
9438 #[cfg(feature = "splat")]
9439 Step::GaussianSplatRenderBackward {
9440 positions_byte_off,
9441 positions_len,
9442 scales_byte_off,
9443 scales_len,
9444 rotations_byte_off,
9445 rotations_len,
9446 opacities_byte_off,
9447 opacities_len,
9448 colors_byte_off,
9449 colors_len,
9450 sh_coeffs_byte_off,
9451 sh_coeffs_len,
9452 meta_byte_off,
9453 d_loss_byte_off,
9454 d_loss_len,
9455 packed_byte_off,
9456 packed_len,
9457 width,
9458 height,
9459 tile_size,
9460 radius_scale,
9461 alpha_cutoff,
9462 max_splat_steps,
9463 transmittance_threshold,
9464 max_list_entries,
9465 loss_grad_clip,
9466 sh_band,
9467 max_anisotropy,
9468 } => {
9469 crate::splat::run_gaussian_splat_render_backward(
9470 &self.arena,
9471 &dev.device,
9472 &dev.queue,
9473 *positions_byte_off as usize,
9474 *positions_len as usize,
9475 *scales_byte_off as usize,
9476 *scales_len as usize,
9477 *rotations_byte_off as usize,
9478 *rotations_len as usize,
9479 *opacities_byte_off as usize,
9480 *opacities_len as usize,
9481 *colors_byte_off as usize,
9482 *colors_len as usize,
9483 *sh_coeffs_byte_off as usize,
9484 *sh_coeffs_len as usize,
9485 *meta_byte_off as usize,
9486 *d_loss_byte_off as usize,
9487 *d_loss_len as usize,
9488 *packed_byte_off as usize,
9489 *packed_len as usize,
9490 *width,
9491 *height,
9492 *tile_size,
9493 *radius_scale,
9494 *alpha_cutoff,
9495 *max_splat_steps,
9496 *transmittance_threshold,
9497 *max_list_entries,
9498 *loss_grad_clip,
9499 *sh_band,
9500 *max_anisotropy,
9501 );
9502 }
9503 _ => break,
9504 }
9505 step_i += 1;
9506 }
9507
9508 self.dump_node_stats_if_requested(dev);
9509
9510 if rlx_ir::env::flag("RLX_WGPU_NAN_TRACE") {
9511 let mut bad_nodes = Vec::new();
9512 for node in self.graph.nodes() {
9513 if !self.arena.has(node.id) {
9514 continue;
9515 }
9516 if matches!(
9518 node.op,
9519 rlx_ir::Op::Input { .. }
9520 | rlx_ir::Op::Param { .. }
9521 | rlx_ir::Op::Constant { .. }
9522 ) {
9523 continue;
9524 }
9525 let data = self.arena.read_f32(&dev.device, &dev.queue, node.id);
9526 let nan_count = data.iter().filter(|v| v.is_nan()).count();
9527 let inf_count = data.iter().filter(|v| v.is_infinite()).count();
9528 if nan_count > 0 || inf_count > 0 {
9529 let first_nan = data.iter().position(|v| v.is_nan());
9531 if let Some(idx) = first_nan {
9532 let lo = idx.saturating_sub(2);
9533 let hi = (idx + 3).min(data.len());
9534 eprintln!(
9535 " node {:?} op={:?} len={} nan={} inf={} \
9536 first_nan_idx={} ctx={:?}",
9537 node.id,
9538 node.op,
9539 data.len(),
9540 nan_count,
9541 inf_count,
9542 idx,
9543 &data[lo..hi]
9544 );
9545 }
9546 bad_nodes.push((node.id, data.len(), nan_count, inf_count));
9547 if bad_nodes.len() >= 3 {
9548 break;
9549 }
9550 }
9551 }
9552 if bad_nodes.is_empty() {
9553 eprintln!("[wgpu-nan-trace] no NaN/Inf in any node — clean run");
9554 } else {
9555 eprintln!(
9556 "[wgpu-nan-trace] first {} bad nodes (above)",
9557 bad_nodes.len()
9558 );
9559 }
9560 }
9561
9562 if rlx_ir::env::flag("RLX_BENCH_DISPATCH_ONLY") {
9563 return self
9564 .graph
9565 .outputs
9566 .iter()
9567 .map(|&id| {
9568 let n = self.graph.node(id).shape.num_elements().unwrap_or(0);
9569 vec![0.0; n]
9570 })
9571 .collect();
9572 }
9573 let out_ids: Vec<_> = self.graph.outputs.clone();
9574 read_f32_many_pooled(
9575 &self.arena,
9576 &dev.device,
9577 &dev.queue,
9578 &out_ids,
9579 &mut self.readback_staging,
9580 )
9581 }
9582}
9583
9584fn dispatch_prologue_nchw(w: u32, h: u32, nc: u32) -> (u32, u32, u32) {
9591 (w.div_ceil(8).max(1), h.div_ceil(8).max(1), nc.max(1))
9592}
9593
9594fn dispatch_dims(threads_total: u32, workgroup_size: u32) -> (u32, u32, u32) {
9595 let groups = threads_total.div_ceil(workgroup_size);
9596 if groups <= 65535 {
9597 (groups, 1, 1)
9598 } else {
9599 let gx = 65535u32;
9600 let gy = groups.div_ceil(gx);
9601 (gx, gy, 1)
9602 }
9603}
9604
9605fn coop_f16_vk_eligible(dev: &wgpu::Device, m: u32, k: u32, n: u32) -> bool {
9627 if rlx_ir::env::flag("RLX_WGPU_NO_COOP_F16_VK")
9628 || rlx_ir::env::flag("RLX_WGPU_COOP_F16_VK_DISABLE")
9629 {
9630 return false;
9631 }
9632 if !rlx_ir::env::flag("RLX_WGPU_COOP_F16_VK_ENABLE") {
9633 return false;
9634 }
9635 m.is_multiple_of(16)
9636 && k.is_multiple_of(16)
9637 && n.is_multiple_of(16)
9638 && dev
9639 .features()
9640 .contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX)
9641 && dev.features().contains(wgpu::Features::SHADER_F16)
9642 && crate::device::coop_discrete_backend()
9643 && crate::device::coop_f16_16x16_supported()
9644}
9645
9646fn step_needs_pass_flush(step: &Step, prev: &Step) -> bool {
9647 match step {
9648 Step::CastF32ToF16 { .. } => matches!(
9649 prev,
9650 Step::Unary {
9651 f16_mirror: false,
9652 ..
9653 }
9654 ),
9655 Step::Matmul {
9656 compute_precision: MatmulCompute::CoopF16Vk,
9657 ..
9658 }
9659 | Step::MatmulQkv {
9660 kind: MatmulQkvKind::CoopF16Vk,
9661 ..
9662 } => matches!(prev, Step::Unary { .. } | Step::CastF32ToF16 { .. }),
9663 _ => false,
9664 }
9665}
9666
9667fn dispatch_wide_f32_matmul(
9668 pass: &mut wgpu::ComputePass<'_>,
9669 mm_w_active: &Kernel,
9670 mm_k: &Kernel,
9671 m_s: u32,
9672 n: u32,
9673 batch: u32,
9674) {
9675 let backend = wgpu_device()
9690 .map(|d| d.backend)
9691 .unwrap_or(wgpu::Backend::Noop);
9692 let is_vulkan_dx12 = matches!(backend, wgpu::Backend::Vulkan | wgpu::Backend::Dx12);
9693 let prefer_small_for_m = is_vulkan_dx12 && m_s < 64;
9694 let use_wide = !prefer_small_for_m && m_s >= 32 && n >= 64;
9695 if use_wide {
9696 pass.set_pipeline(&mm_w_active.pipeline);
9697 let (gx, gy) = if is_vulkan_dx12 {
9698 (n.div_ceil(64), m_s.div_ceil(64))
9699 } else {
9700 (n.div_ceil(64), m_s.div_ceil(32))
9701 };
9702 pass.dispatch_workgroups(gx, gy, batch);
9703 } else {
9704 pass.set_pipeline(&mm_k.pipeline);
9705 pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), batch);
9706 }
9707}
9708
9709fn coop_f16_vk_bind_group(exe: &WgpuExecutable, gpu_bi: usize, use_wide: bool) -> &wgpu::BindGroup {
9710 if use_wide {
9711 exe.coop_f16_vk_wide_bind_groups
9712 .get(&gpu_bi)
9713 .unwrap_or(&exe.bind_groups[gpu_bi])
9714 } else {
9715 &exe.bind_groups[gpu_bi]
9716 }
9717}
9718
9719fn require_equal_shapes(graph: &Graph, ids: &[NodeId], op_name: &str) {
9720 let s0 = graph.node(ids[0]).shape.num_elements().unwrap_or(0);
9721 for &id in &ids[1..] {
9722 let si = graph.node(id).shape.num_elements().unwrap_or(0);
9723 if si != s0 {
9724 panic!(
9725 "rlx-wgpu {op_name}: broadcasting not yet implemented; \
9726 inputs must have the same element count (got {s0} vs {si})"
9727 );
9728 }
9729 }
9730}
9731
9732fn arena_whole_arena_bind(arena: &Arena, max_binding: u64) -> Option<(u64, u64)> {
9734 let need = arena.size as u64;
9735 if need > max_binding {
9736 return None;
9737 }
9738 let buf_bytes = arena.buffer.size();
9740 let size = need.min(buf_bytes).max(256);
9741 Some((0, size))
9742}
9743
9744fn arena_window_for_nodes(dev: &wgpu::Device, arena: &Arena, ids: &[NodeId]) -> (u64, u64) {
9745 const ALIGN: u64 = 256;
9747 let max_binding = dev.limits().max_storage_buffer_binding_size;
9748 if let Some(w) = arena_whole_arena_bind(arena, max_binding) {
9749 return w;
9750 }
9751 let mut lo: u64 = u64::MAX;
9752 let mut hi: u64 = 0;
9753 for &id in ids {
9754 let off = arena.offset(id) as u64;
9755 let len = arena.len_of(id) as u64;
9756 lo = lo.min(off);
9757 hi = hi.max(off.saturating_add(len));
9758 }
9759 if lo == u64::MAX {
9760 return (0, max_binding.max(256));
9761 }
9762 let span = hi.saturating_sub(lo).max(1);
9763 if span > max_binding {
9764 let mut details = String::new();
9765 for &id in ids.iter().take(6) {
9766 let off = arena.offset(id);
9767 let len = arena.len_of(id);
9768 details.push_str(&format!(" id={id:?}@{off}+{len};"));
9769 }
9770 panic!(
9771 "rlx-wgpu: op needs {} bytes of arena span (>{});{}",
9772 span, max_binding, details
9773 );
9774 }
9775 let mut base = (lo / ALIGN) * ALIGN;
9776 let mut size = span.div_ceil(ALIGN) * ALIGN;
9779 size = size.max(256).min(max_binding);
9780 if base.saturating_add(size) > arena.size as u64 {
9781 base = (arena.size as u64).saturating_sub(size);
9782 base = (base / ALIGN) * ALIGN;
9783 }
9784 if base > lo || base.saturating_add(size) < hi {
9785 base = (lo / ALIGN) * ALIGN;
9786 size = hi.saturating_sub(base).div_ceil(ALIGN) * ALIGN;
9787 size = size.max(256).min(max_binding);
9788 if base.saturating_add(size) > arena.size as u64 {
9789 base = hi.saturating_sub(size);
9790 base = (base / ALIGN) * ALIGN;
9791 }
9792 }
9793 (base, size)
9794}
9795
9796fn arena_local_off_f32(arena: &Arena, id: NodeId, base: u64) -> u32 {
9797 (((arena.offset(id) as u64).saturating_sub(base)) / 4) as u32
9798}
9799
9800#[allow(clippy::too_many_arguments)]
9809fn run_gather_split(
9810 arena: &Arena,
9811 device: &wgpu::Device,
9812 queue: &wgpu::Queue,
9813 n_out: u32,
9814 n_idx: u32,
9815 dim: u32,
9816 vocab: u32,
9817 table_byte_off: usize,
9818 idx_byte_off: usize,
9819 out_byte_off: usize,
9820) {
9821 const ALIGN: u64 = 256;
9822 let arena_size = arena.size as u64;
9823 let max_bind = device.limits().max_storage_buffer_binding_size;
9824
9825 let t0 = table_byte_off as u64;
9827 let t_bytes = (vocab as u64) * (dim as u64) * 4;
9828 let t_base = (t0 / ALIGN) * ALIGN;
9829 let t_size = ((t0 + t_bytes - t_base).div_ceil(16) * 16).min(arena_size - t_base);
9830
9831 let i0 = idx_byte_off as u64;
9833 let i_bytes = ((n_idx as u64) * 4).max(4);
9834 let i_base = (i0 / ALIGN) * ALIGN;
9835 let i_size = ((i0 + i_bytes - i_base).div_ceil(16) * 16).min(arena_size - i_base);
9836
9837 assert!(
9838 t_size <= max_bind && i_size <= max_bind,
9839 "rlx-wgpu gather_split: window too large (table={t_size}, idx={i_size}, max={max_bind})"
9840 );
9841
9842 let out_bytes = ((n_out as u64) * 4).max(4);
9844 let out_buf = device.create_buffer(&wgpu::BufferDescriptor {
9845 label: Some("rlx-wgpu gather_split out"),
9846 size: out_bytes.div_ceil(16) * 16,
9847 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
9848 mapped_at_creation: false,
9849 });
9850
9851 let p = GatherParams {
9852 n_out,
9853 n_idx,
9854 dim,
9855 vocab,
9856 in_off: ((t0 - t_base) / 4) as u32,
9857 idx_off: ((i0 - i_base) / 4) as u32,
9858 out_off: 0,
9859 _p0: 0,
9860 };
9861 let u = device.create_buffer(&wgpu::BufferDescriptor {
9862 label: Some("rlx-wgpu gather_split uniform"),
9863 size: std::mem::size_of::<GatherParams>() as u64,
9864 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
9865 mapped_at_creation: false,
9866 });
9867 queue.write_buffer(&u, 0, bytemuck::bytes_of(&p));
9868
9869 let gk = gather_split_kernel(device);
9870 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
9871 label: Some("rlx-wgpu gather_split bg"),
9872 layout: &gk.bgl,
9873 entries: &[
9874 wgpu::BindGroupEntry {
9875 binding: 0,
9876 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
9877 buffer: &arena.buffer,
9878 offset: t_base,
9879 size: wgpu::BufferSize::new(t_size),
9880 }),
9881 },
9882 wgpu::BindGroupEntry {
9883 binding: 1,
9884 resource: u.as_entire_binding(),
9885 },
9886 wgpu::BindGroupEntry {
9887 binding: 2,
9888 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
9889 buffer: &arena.buffer,
9890 offset: i_base,
9891 size: wgpu::BufferSize::new(i_size),
9892 }),
9893 },
9894 wgpu::BindGroupEntry {
9895 binding: 3,
9896 resource: out_buf.as_entire_binding(),
9897 },
9898 ],
9899 });
9900
9901 let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
9902 label: Some("rlx-wgpu gather_split"),
9903 });
9904 {
9905 let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
9906 label: Some("rlx-wgpu gather_split pass"),
9907 ..Default::default()
9908 });
9909 pass.set_pipeline(&gk.pipeline);
9910 pass.set_bind_group(0, &bg, &[]);
9911 let (gx, gy, gz) = dispatch_dims(n_out, 64);
9912 pass.dispatch_workgroups(gx, gy, gz);
9913 }
9914 enc.copy_buffer_to_buffer(&out_buf, 0, &arena.buffer, out_byte_off as u64, out_bytes);
9916 queue.submit(std::iter::once(enc.finish()));
9917}
9918
9919fn arena_tensor_in_window(arena: &Arena, id: NodeId, base: u64, size: u64) -> bool {
9920 let src = arena.offset(id) as u64;
9921 let len = arena.len_of(id) as u64;
9922 src >= base && src.saturating_add(len) <= base.saturating_add(size)
9923}
9924
9925fn arena_tensors_overlap(arena: &Arena, a: NodeId, b: NodeId) -> bool {
9927 if a == b {
9928 return true;
9929 }
9930 let (a0, al) = (arena.offset(a) as u64, arena.len_of(a) as u64);
9931 let (b0, bl) = (arena.offset(b) as u64, arena.len_of(b) as u64);
9932 if al == 0 || bl == 0 {
9933 return false;
9934 }
9935 let a1 = a0.saturating_add(al);
9936 let b1 = b0.saturating_add(bl);
9937 a0 < b1 && b0 < a1
9938}
9939
9940fn matmul_b_from_f16(precision: MatmulCompute, b_is_param: bool) -> bool {
9944 b_is_param
9945 && matches!(
9946 precision,
9947 MatmulCompute::F16 | MatmulCompute::Coop16 | MatmulCompute::CoopF16Vk
9948 )
9949}
9950
9951fn arena_matmul_bind_window(
9954 device: &wgpu::Device,
9955 arena: &Arena,
9956 graph: &Graph,
9957 param_offsets: &HashMap<String, NodeId>,
9958 out_id: NodeId,
9959 a_id: NodeId,
9960 b_id: NodeId,
9961 b_in_arena: bool,
9962) -> (u64, u64, bool) {
9963 let max_binding = device.limits().max_storage_buffer_binding_size;
9964 if let Some((base, size)) = arena_whole_arena_bind(arena, max_binding) {
9965 return (base, size, false);
9966 }
9967 if !b_in_arena {
9968 let (base, size) = arena_window_for_nodes(device, arena, &[out_id, a_id]);
9975 return (base, size, false);
9976 }
9977 let ids = [out_id, a_id, b_id];
9978 let all_fits = arena_span_bytes(arena, &ids) <= max_binding;
9979 let b_bytes = arena.len_of(b_id) as u64;
9980 let b_is_param = tensor_is_graph_param(graph, param_offsets, b_id);
9981 let param_anchor =
9982 b_is_param && b_bytes <= max_binding && (!all_fits || b_bytes > ARENA_STAGE_CAP);
9983 let (mut base, mut size) = if param_anchor {
9984 arena_window_for_nodes(device, arena, &[b_id])
9985 } else if all_fits {
9986 arena_window_for_nodes(device, arena, &ids)
9987 } else {
9988 arena_window_for_nodes(device, arena, &[out_id])
9989 };
9990 let param_anchor = param_anchor
9991 || (b_is_param
9992 && b_bytes <= max_binding
9993 && !arena_tensor_in_window(arena, b_id, base, size));
9994 if param_anchor && !arena_tensor_in_window(arena, b_id, base, size) {
9995 (base, size) = arena_window_for_nodes(device, arena, &[b_id]);
9996 }
9997 (base, size, param_anchor)
9998}
9999
10000fn arena_expand_bind_window(
10003 arena: &Arena,
10004 ids: &[NodeId],
10005 base: &mut u64,
10006 size: &mut u64,
10007 max_binding: u64,
10008) {
10009 const ALIGN: u64 = 256;
10010 let mut lo = *base;
10011 let mut hi = base.saturating_add(*size);
10012 for &id in ids {
10013 let off = arena.offset(id) as u64;
10014 let len = arena.len_of(id) as u64;
10015 lo = lo.min(off);
10016 hi = hi.max(off.saturating_add(len));
10017 }
10018 let span = hi.saturating_sub(lo).max(1);
10019 if span > max_binding {
10020 return;
10021 }
10022 *base = (lo / ALIGN) * ALIGN;
10023 *size = span.div_ceil(ALIGN) * ALIGN;
10024 *size = (*size).max(256).min(max_binding);
10025 if (*base).saturating_add(*size) > arena.size as u64 {
10026 *base = (arena.size as u64).saturating_sub(*size);
10027 *base = (*base / ALIGN) * ALIGN;
10028 }
10029}
10030
10031fn arena_off_in_bind_window(
10032 graph: &Graph,
10033 param_offsets: &HashMap<String, NodeId>,
10034 device: &wgpu::Device,
10035 arena: &Arena,
10036 schedule: &mut Vec<Step>,
10037 scratch: &mut u64,
10038 id: NodeId,
10039 base: &mut u64,
10040 size: &mut u64,
10041) -> u32 {
10042 let max_binding = device.limits().max_storage_buffer_binding_size;
10043 if let Some((b, s)) = arena_whole_arena_bind(arena, max_binding) {
10044 *base = b;
10045 *size = s;
10046 return arena_local_off_f32(arena, id, b);
10047 }
10048 if arena_tensor_in_window(arena, id, *base, *size) {
10049 arena_local_off_f32(arena, id, *base)
10050 } else {
10051 let len = arena.len_of(id) as u64;
10052 if tensor_is_graph_param(graph, param_offsets, id) && len > max_binding {
10053 panic!(
10054 "rlx-wgpu: param node {:?} ({} bytes) exceeds max_storage_buffer_binding_size \
10055 ({max_binding}); split weights or use f16 shadow binds",
10056 id, len
10057 );
10058 }
10059 if len > ARENA_STAGE_CAP {
10060 let op = &graph.node(id).op;
10061 panic!(
10062 "rlx-wgpu: bind_window would stage {} bytes for {:?} op={op:?} \
10063 (off={}, base={}, bind_size={})",
10064 len,
10065 id,
10066 arena.offset(id),
10067 *base,
10068 *size,
10069 );
10070 }
10071 arena_off_in_window_or_stage(arena, schedule, scratch, base, size, max_binding, id)
10072 }
10073}
10074
10075fn arena_multi_op_window(
10079 dev: &wgpu::Device,
10080 arena: &Arena,
10081 graph: &Graph,
10082 param_offsets: &HashMap<String, NodeId>,
10083 _schedule: &mut Vec<Step>,
10084 scratch: &mut u64,
10085 ids: &[NodeId],
10086) -> (u64, u64, bool) {
10087 let max_binding = dev.limits().max_storage_buffer_binding_size;
10088 if let Some((base, size)) = arena_whole_arena_bind(arena, max_binding) {
10089 *scratch = arena.scratch_off as u64;
10090 return (base, size, false);
10091 }
10092 let param_anchor = if arena_span_bytes(arena, ids) > max_binding {
10093 ids.iter()
10094 .find(|&&id| {
10095 let nbytes = arena.len_of(id) as u64;
10096 tensor_is_graph_param(graph, param_offsets, id) && nbytes <= max_binding
10097 })
10098 .copied()
10099 } else {
10100 None
10101 };
10102 let mut param_anchored = param_anchor.is_some();
10103 let (mut base, mut size) = if arena_span_bytes(arena, ids) <= max_binding {
10104 arena_window_for_nodes(dev, arena, ids)
10105 } else if let Some(id) = param_anchor {
10106 arena_window_for_nodes(dev, arena, &[id])
10107 } else {
10108 arena_window_for_nodes(dev, arena, &[ids[0]])
10109 };
10110 if let Some(id) = param_anchor {
10111 if !arena_tensor_in_window(arena, id, base, size) {
10112 (base, size) = arena_window_for_nodes(dev, arena, &[id]);
10113 }
10114 param_anchored = true;
10115 } else {
10116 for &id in ids {
10117 let nbytes = arena.len_of(id) as u64;
10118 if tensor_is_graph_param(graph, param_offsets, id)
10119 && nbytes <= max_binding
10120 && !arena_tensor_in_window(arena, id, base, size)
10121 {
10122 (base, size) = arena_window_for_nodes(dev, arena, &[id]);
10123 param_anchored = true;
10124 break;
10125 }
10126 }
10127 }
10128 *scratch = arena.scratch_off as u64;
10129 if param_anchored {
10130 arena_ensure_scratch_in_window(scratch, base, size);
10131 }
10132 (base, size, param_anchored)
10133}
10134
10135fn arena_bind_window_covering_scratch_if_needed(
10136 arena: &Arena,
10137 base: u64,
10138 size: u64,
10139 scratch: u64,
10140) -> u64 {
10141 if scratch <= arena.scratch_off as u64 {
10144 return base;
10145 }
10146 if scratch >= base && scratch.saturating_add(ARENA_STAGE_CAP) <= base.saturating_add(size) {
10147 return base;
10148 }
10149 arena_window_covering_scratch(arena, base, size)
10150}
10151
10152fn arena_ensure_scratch_in_window(scratch: &mut u64, base: u64, size: u64) {
10155 let cap = ARENA_STAGE_CAP.min(size);
10156 let end = base.saturating_add(size);
10157 if *scratch < base || scratch.saturating_add(cap) > end {
10158 *scratch = end.saturating_sub(cap);
10159 *scratch = (*scratch / 256) * 256;
10160 }
10161}
10162
10163#[allow(dead_code)]
10164fn arena_off_for_window(
10165 arena: &Arena,
10166 schedule: &mut Vec<Step>,
10167 scratch: &mut u64,
10168 id: NodeId,
10169 _window_ids: &[NodeId],
10170 mut base: u64,
10171 mut size: u64,
10172 max_binding: u64,
10173 _fits_in_one_binding: bool,
10174) -> u32 {
10175 let src = arena.offset(id) as u64;
10176 let len = arena.len_of(id) as u64;
10177 if src >= base && src.saturating_add(len) <= base.saturating_add(size) {
10178 arena_local_off_f32(arena, id, base)
10179 } else {
10180 arena_off_in_window_or_stage(
10181 arena,
10182 schedule,
10183 scratch,
10184 &mut base,
10185 &mut size,
10186 max_binding,
10187 id,
10188 )
10189 }
10190}
10191
10192fn f16_shadow_bind_range(arena_base: u64, arena_size: u64, f16_buf_bytes: u64) -> (u64, u64) {
10194 const ALIGN: u64 = 256;
10195 let mut base = (arena_base / 2 / ALIGN) * ALIGN;
10196 let mut size = (arena_size / 2).div_ceil(ALIGN) * ALIGN;
10197 size = size.max(256).min(f16_buf_bytes);
10198 if base.saturating_add(size) > f16_buf_bytes {
10199 base = f16_buf_bytes.saturating_sub(size);
10200 base = (base / ALIGN) * ALIGN;
10201 }
10202 (base, size)
10203}
10204
10205fn f16_weight_bind_range(
10208 dev: &wgpu::Device,
10209 f16_buf_bytes: u64,
10210 b_off: u32,
10211 k: u32,
10212 n: u32,
10213 batch: u32,
10214 b_batch_stride: u32,
10215) -> (u64, u64, u32) {
10216 const ALIGN: u64 = 256;
10217 let max_binding = dev.limits().max_storage_buffer_binding_size;
10218 let b0 = b_off as u64;
10219 let span = (k as u64).saturating_mul(n as u64);
10220 let batch_n = batch.max(1) as u64;
10221 let stride = if batch_n > 1 {
10222 b_batch_stride as u64
10223 } else {
10224 span
10225 };
10226 let hi_elems = b0
10227 .saturating_add((batch_n - 1).saturating_mul(stride))
10228 .saturating_add(span);
10229 let lo_byte = b0.saturating_mul(2);
10230 let hi_byte = hi_elems.saturating_mul(2).saturating_add(8);
10231 let need = hi_byte.saturating_sub(lo_byte).max(1);
10232 if need > max_binding {
10233 panic!(
10234 "rlx-wgpu: f16 weight region needs {need} bytes (> {max_binding}); \
10235 matmul k={k} n={n} batch={batch}"
10236 );
10237 }
10238 let mut base = (lo_byte / ALIGN) * ALIGN;
10239 let mut size = need.div_ceil(ALIGN) * ALIGN;
10240 size = size.max(256).min(max_binding).min(f16_buf_bytes);
10241 if base.saturating_add(size) < hi_byte {
10242 base = hi_byte.saturating_sub(size);
10243 base = (base / ALIGN) * ALIGN;
10244 }
10245 if base.saturating_add(size) > f16_buf_bytes {
10246 base = f16_buf_bytes.saturating_sub(size);
10247 base = (base / ALIGN) * ALIGN;
10248 }
10249 let rebased = b_off.saturating_sub((base / 2) as u32);
10250 (base, size, rebased)
10251}
10252
10253const ARENA_STAGE_CAP: u64 = 256 * 1024 * 1024;
10254
10255const CONV2D_TILE: u32 = 4;
10258
10259fn arena_off_in_window_or_stage(
10262 arena: &Arena,
10263 schedule: &mut Vec<Step>,
10264 scratch: &mut u64,
10265 base: &mut u64,
10266 size: &mut u64,
10267 max_binding: u64,
10268 id: NodeId,
10269) -> u32 {
10270 let src = arena.offset(id) as u64;
10271 let len = arena.len_of(id) as u64;
10272 if src >= *base && src.saturating_add(len) <= (*base).saturating_add(*size) {
10273 return arena_local_off_f32(arena, id, *base);
10274 }
10275 if len > ARENA_STAGE_CAP {
10276 panic!(
10277 "rlx-wgpu: cannot stage {} bytes for node {:?} (cap {ARENA_STAGE_CAP})",
10278 len, id
10279 );
10280 }
10281 let aligned = len.div_ceil(256) * 256;
10282 let dst = *scratch;
10283 *scratch = scratch.saturating_add(aligned);
10284 schedule.push(Step::BufferCopy {
10285 src_byte_off: src,
10286 dst_byte_off: dst,
10287 bytes: len as u32,
10288 });
10289 let lo = (*base).min(dst);
10290 let hi = (*base)
10291 .saturating_add(*size)
10292 .max(dst.saturating_add(aligned));
10293 let span = hi.saturating_sub(lo).max(1);
10294 if span <= max_binding {
10295 const ALIGN: u64 = 256;
10296 *base = (lo / ALIGN) * ALIGN;
10297 *size = span.div_ceil(ALIGN) * ALIGN;
10298 *size = (*size).max(256).min(max_binding);
10299 if (*base).saturating_add(*size) > arena.size as u64 {
10300 *base = (arena.size as u64).saturating_sub(*size);
10301 *base = (*base / ALIGN) * ALIGN;
10302 }
10303 }
10304 if arena_tensor_in_window(arena, id, *base, *size) {
10305 arena_local_off_f32(arena, id, *base)
10306 } else {
10307 ((dst.saturating_sub(*base)) / 4) as u32
10308 }
10309}
10310
10311fn arena_window_covering_scratch(arena: &Arena, base: u64, size: u64) -> u64 {
10313 let scratch = arena.scratch_off as u64;
10314 if scratch >= base && scratch.saturating_add(ARENA_STAGE_CAP) <= base.saturating_add(size) {
10315 return base;
10316 }
10317 let new_base = (arena.size as u64).saturating_sub(size);
10318 (new_base / 256) * 256
10319}
10320
10321fn arena_span_bytes(arena: &Arena, ids: &[NodeId]) -> u64 {
10322 let mut lo: u64 = u64::MAX;
10323 let mut hi: u64 = 0;
10324 for &id in ids {
10325 let off = arena.offset(id) as u64;
10326 let len = arena.len_of(id) as u64;
10327 lo = lo.min(off);
10328 hi = hi.max(off.saturating_add(len));
10329 }
10330 if lo == u64::MAX {
10331 0
10332 } else {
10333 hi.saturating_sub(lo)
10334 }
10335}
10336
10337#[allow(dead_code)]
10338fn bind_two(
10339 device: &wgpu::Device,
10340 kernel: &Kernel,
10341 buf0: &wgpu::Buffer,
10342 buf1: &wgpu::Buffer,
10343) -> wgpu::BindGroup {
10344 let max_binding = device.limits().max_storage_buffer_binding_size;
10345 if buf0.size() > max_binding {
10346 panic!(
10347 "rlx-wgpu: bind_two buffer {} bytes exceeds max_storage_buffer_binding_size {}; \
10348 use bind_two_buf0_window or bind_op_output_window",
10349 buf0.size(),
10350 max_binding
10351 );
10352 }
10353 device.create_bind_group(&wgpu::BindGroupDescriptor {
10354 label: Some("rlx-wgpu bg"),
10355 layout: &kernel.bgl,
10356 entries: &[
10357 wgpu::BindGroupEntry {
10358 binding: 0,
10359 resource: buf0.as_entire_binding(),
10360 },
10361 wgpu::BindGroupEntry {
10362 binding: 1,
10363 resource: buf1.as_entire_binding(),
10364 },
10365 ],
10366 })
10367}
10368
10369fn bind_op_output_window(
10373 device: &wgpu::Device,
10374 kernel: &Kernel,
10375 arena: &Arena,
10376 out_id: NodeId,
10377 params: &wgpu::Buffer,
10378) -> wgpu::BindGroup {
10379 bind_op_window(device, kernel, arena, &[out_id], params)
10380}
10381
10382fn bind_op_window(
10383 device: &wgpu::Device,
10384 kernel: &Kernel,
10385 arena: &Arena,
10386 ids: &[NodeId],
10387 params: &wgpu::Buffer,
10388) -> wgpu::BindGroup {
10389 let max_binding = device.limits().max_storage_buffer_binding_size;
10390 let (base, size) = if arena_span_bytes(arena, ids) <= max_binding {
10391 arena_window_for_nodes(device, arena, ids)
10392 } else {
10393 arena_window_for_nodes(device, arena, &[ids[0]])
10394 };
10395 bind_two_buf0_window(device, kernel, &arena.buffer, base, size, params)
10396}
10397
10398fn bind_two_buf0_window(
10399 device: &wgpu::Device,
10400 kernel: &Kernel,
10401 buf0: &wgpu::Buffer,
10402 buf0_base: u64,
10403 buf0_size: u64,
10404 buf1: &wgpu::Buffer,
10405) -> wgpu::BindGroup {
10406 device.create_bind_group(&wgpu::BindGroupDescriptor {
10407 label: Some("rlx-wgpu bg window"),
10408 layout: &kernel.bgl,
10409 entries: &[
10410 wgpu::BindGroupEntry {
10411 binding: 0,
10412 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
10413 buffer: buf0,
10414 offset: buf0_base,
10415 size: NonZeroU64::new(buf0_size),
10416 }),
10417 },
10418 wgpu::BindGroupEntry {
10419 binding: 1,
10420 resource: buf1.as_entire_binding(),
10421 },
10422 ],
10423 })
10424}
10425
10426fn derive_matmul_compute(
10449 dev: &wgpu::Device,
10450 graph: &Graph,
10451 mirror_acts: &HashSet<NodeId>,
10452 a_id: NodeId,
10453 b_id: NodeId,
10454 m: u32,
10455 k: u32,
10456 n: u32,
10457) -> MatmulCompute {
10458 if rlx_ir::env::flag("RLX_WGPU_MATMUL_F32_ONLY") {
10459 return MatmulCompute::F32;
10460 }
10461 use rlx_ir::DType;
10462 let a_dt = graph.node(a_id).shape.dtype();
10463 let b_dt = graph.node(b_id).shape.dtype();
10464 let any_low =
10465 matches!(a_dt, DType::F16 | DType::BF16) || matches!(b_dt, DType::F16 | DType::BF16);
10466 let coop16_aligned = m.is_multiple_of(32) && k.is_multiple_of(8) && n.is_multiple_of(32);
10476 let coop_f32_metal_aligned = k.is_multiple_of(8) && n.is_multiple_of(32);
10477 let coop_f32_portable_aligned = k.is_multiple_of(8) && n.is_multiple_of(8);
10478 let has_coop = dev
10479 .features()
10480 .contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX);
10481 let backend = crate::device::wgpu_device().map(|d| d.backend);
10482 if any_low
10487 && has_coop
10488 && dev.features().contains(wgpu::Features::SHADER_F16)
10489 && traces_to_param(graph, b_id)
10490 && coop16_aligned
10491 {
10492 return MatmulCompute::Coop16;
10493 }
10494 if !any_low && coop_f16_vk_eligible(dev, m, k, n) {
10495 if traces_to_param(graph, b_id)
10496 && !mirror_acts.contains(&a_id)
10497 && !mirror_acts.contains(&b_id)
10498 {
10499 return MatmulCompute::CoopF16Vk;
10500 }
10501 }
10502 let disabled = rlx_ir::env::flag("RLX_WGPU_NO_COOP_F32");
10511 let forced = rlx_ir::env::flag("RLX_WGPU_FORCE_COOP_F32");
10512 let metal_coop = !disabled
10513 && has_coop
10514 && coop_f32_metal_aligned
10515 && traces_to_param(graph, b_id)
10516 && (forced || matches!(backend, Some(wgpu::Backend::Metal)));
10517 let vulkan_coop = !disabled
10518 && has_coop
10519 && coop_f32_portable_aligned
10520 && traces_to_param(graph, b_id)
10521 && crate::device::coop_discrete_backend()
10522 && crate::device::coop_f32_8x8_supported();
10523 if metal_coop
10524 || vulkan_coop
10525 || (forced
10526 && has_coop
10527 && traces_to_param(graph, b_id)
10528 && (coop_f32_metal_aligned || coop_f32_portable_aligned))
10529 {
10530 return MatmulCompute::CoopF32;
10531 }
10532 MatmulCompute::F32
10533}
10534
10535#[allow(dead_code)]
10555fn detect_qkv_narrow_pattern(
10556 graph: &Graph,
10557 q_id: NodeId,
10558 k_id: NodeId,
10559 v_id: NodeId,
10560) -> Option<(NodeId, u32)> {
10561 let unwrap_narrow = |id: NodeId| -> Option<(NodeId, usize, usize, usize)> {
10562 let node = graph.node(id);
10563 match &node.op {
10564 Op::Narrow { axis, start, len } => Some((node.inputs[0], *axis, *start, *len)),
10565 _ => None,
10566 }
10567 };
10568 let (q_src, q_axis, q_start, q_len) = unwrap_narrow(q_id)?;
10569 let (k_src, k_axis, k_start, k_len) = unwrap_narrow(k_id)?;
10570 let (v_src, v_axis, v_start, v_len) = unwrap_narrow(v_id)?;
10571 if q_src != k_src || k_src != v_src {
10573 return None;
10574 }
10575 if q_len != k_len || k_len != v_len {
10577 return None;
10578 }
10579 if q_start != 0 || k_start != q_len || v_start != q_len * 2 {
10581 return None;
10582 }
10583 let src_rank = graph.node(q_src).shape.dims().len();
10585 if q_axis + 1 != src_rank || k_axis + 1 != src_rank || v_axis + 1 != src_rank {
10586 return None;
10587 }
10588 Some((q_src, q_len as u32))
10589}
10590
10591fn detect_residual_ln_tee_pattern(
10621 graph: &Graph,
10622) -> (
10623 HashMap<NodeId, (NodeId, NodeId, NodeId, NodeId, NodeId)>,
10624 HashSet<NodeId>,
10625) {
10626 use rlx_ir::op::BinaryOp;
10627 let mut consumers: HashMap<NodeId, usize> = HashMap::new();
10629 for node in graph.nodes() {
10630 for &input in &node.inputs {
10631 *consumers.entry(input).or_insert(0) += 1;
10632 }
10633 }
10634 for &out in &graph.outputs {
10635 *consumers.entry(out).or_insert(0) += 1;
10636 }
10637
10638 let mut ln_to_tee = HashMap::new();
10639 let mut skip_adds = HashSet::new();
10640 for node in graph.nodes() {
10641 let Op::LayerNorm { axis: _, eps: _ } = &node.op else {
10642 continue;
10643 };
10644 if node.inputs.len() < 3 {
10645 continue;
10646 } let in_id = node.inputs[0];
10648 let in_node = graph.node(in_id);
10649 if !matches!(in_node.op, Op::Binary(BinaryOp::Add)) {
10650 continue;
10651 }
10652 if consumers.get(&in_id).copied().unwrap_or(0) < 2 {
10655 continue;
10656 }
10657 if in_node.inputs.len() != 2 {
10660 continue;
10661 }
10662 let h_id = in_node.inputs[0];
10663 let delta_id = in_node.inputs[1];
10664 if graph.node(h_id).shape.dims() != node.shape.dims() {
10665 continue;
10666 }
10667 if graph.node(delta_id).shape.dims() != node.shape.dims() {
10668 continue;
10669 }
10670 let gamma_id = node.inputs[1];
10671 let beta_id = node.inputs[2];
10672 ln_to_tee.insert(node.id, (h_id, delta_id, gamma_id, beta_id, in_id));
10673 skip_adds.insert(in_id);
10674 }
10675 (ln_to_tee, skip_adds)
10676}
10677
10678fn detect_split_qkv_pattern(graph: &Graph) -> HashMap<NodeId, (NodeId, NodeId, NodeId)> {
10679 let mut consumers: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
10681 for node in graph.nodes() {
10682 for &input in &node.inputs {
10683 consumers.entry(input).or_default().push(node.id);
10684 }
10685 }
10686 for &out_id in &graph.outputs {
10689 consumers.entry(out_id).or_default().push(NodeId(u32::MAX));
10690 }
10691
10692 let mut result = HashMap::new();
10693 for node in graph.nodes() {
10694 if !matches!(node.op, Op::FusedMatMulBiasAct { activation: None }) {
10695 continue;
10696 }
10697 let cs = match consumers.get(&node.id) {
10698 Some(c) if c.len() == 3 => c,
10699 _ => continue,
10700 };
10701 let dims = node.shape.dims();
10702 if dims.is_empty() {
10703 continue;
10704 }
10705 let last_axis = dims.len() - 1;
10706 let n = dims[last_axis].unwrap_static();
10707 if n % 3 != 0 {
10708 continue;
10709 }
10710 let head_width = n / 3;
10711
10712 let mut narrows: Vec<(usize, NodeId)> = Vec::with_capacity(3);
10714 let mut all_match = true;
10715 for &c in cs {
10716 let cn = graph.node(c);
10717 match cn.op {
10718 Op::Narrow { axis, start, len }
10719 if axis == last_axis && len == head_width && cn.inputs[0] == node.id =>
10720 {
10721 narrows.push((start, c));
10722 }
10723 _ => {
10724 all_match = false;
10725 break;
10726 }
10727 }
10728 }
10729 if !all_match {
10730 continue;
10731 }
10732 narrows.sort_by_key(|&(start, _)| start);
10733 if narrows[0].0 != 0 || narrows[1].0 != head_width || narrows[2].0 != 2 * head_width {
10734 continue;
10735 }
10736 result.insert(node.id, (narrows[0].1, narrows[1].1, narrows[2].1));
10737 }
10738 result
10739}
10740
10741fn node_is_arena_param(param_offsets: &HashMap<String, NodeId>, id: NodeId) -> bool {
10747 param_offsets.values().any(|&nid| nid == id)
10748}
10749
10750fn traces_to_param(graph: &Graph, mut id: NodeId) -> bool {
10751 loop {
10752 let node = graph.node(id);
10753 match &node.op {
10754 Op::Param { .. } => return true,
10755 Op::Cast { .. } | Op::Reshape { .. } | Op::Transpose { .. } => {
10756 if node.inputs.is_empty() {
10757 return false;
10758 }
10759 id = node.inputs[0];
10760 }
10761 _ => return false,
10762 }
10763 }
10764}
10765
10766fn tensor_is_graph_param(
10767 graph: &Graph,
10768 param_offsets: &HashMap<String, NodeId>,
10769 id: NodeId,
10770) -> bool {
10771 node_is_arena_param(param_offsets, id) || traces_to_param(graph, id)
10772}
10773
10774fn traces_to_input(graph: &Graph, mut id: NodeId) -> bool {
10775 loop {
10776 let node = graph.node(id);
10777 match &node.op {
10778 Op::Input { .. } => return true,
10779 Op::Cast { .. } | Op::Reshape { .. } => {
10780 if node.inputs.is_empty() {
10781 return false;
10782 }
10783 id = node.inputs[0];
10784 }
10785 _ => return false,
10786 }
10787 }
10788}
10789
10790fn schedule_uses_coop_f16_vk(schedule: &[Step]) -> bool {
10793 schedule.iter().any(|s| {
10794 matches!(
10795 s,
10796 Step::Matmul {
10797 compute_precision: MatmulCompute::CoopF16Vk,
10798 ..
10799 } | Step::MatmulQkv {
10800 kind: MatmulQkvKind::CoopF16Vk,
10801 ..
10802 }
10803 )
10804 })
10805}
10806
10807fn register_coop_f16_vk_b_param(
10808 map: &mut HashMap<u32, String>,
10809 param_offsets: &HashMap<String, NodeId>,
10810 b_id: NodeId,
10811 b_off_f32: u32,
10812 compute: MatmulCompute,
10813) {
10814 if compute != MatmulCompute::CoopF16Vk {
10815 return;
10816 }
10817 for (name, &id) in param_offsets {
10818 if id == b_id {
10819 map.insert(b_off_f32, name.clone());
10820 return;
10821 }
10822 }
10823}
10824
10825fn tensor_host_name(
10826 input_offsets: &HashMap<String, NodeId>,
10827 param_offsets: &HashMap<String, NodeId>,
10828 id: NodeId,
10829) -> String {
10830 for (name, &nid) in input_offsets {
10831 if nid == id {
10832 return name.clone();
10833 }
10834 }
10835 for (name, &nid) in param_offsets {
10836 if nid == id {
10837 return name.clone();
10838 }
10839 }
10840 panic!("rlx-wgpu: CoopF16Vk host activation source {id} is not an input or param");
10841}
10842
10843fn host_tensor_f32<'a>(
10844 name: &str,
10845 inputs: &'a [(&str, &[f32])],
10846 stashed_params: &'a HashMap<String, Vec<f32>>,
10847) -> Option<&'a [f32]> {
10848 inputs
10849 .iter()
10850 .find(|(n, _)| *n == name)
10851 .map(|(_, d)| *d)
10852 .or_else(|| stashed_params.get(name).map(|v| v.as_slice()))
10853}
10854
10855fn apply_activation_host(act: Activation, data: &[f32]) -> Vec<f32> {
10856 data.iter()
10857 .map(|&x| match act {
10858 Activation::Relu => x.max(0.0),
10859 Activation::Sigmoid => 1.0 / (1.0 + (-x).exp()),
10860 Activation::Tanh => x.tanh(),
10861 Activation::Exp => x.exp(),
10862 Activation::Log => x.ln(),
10863 Activation::Sqrt => x.sqrt(),
10864 Activation::Rsqrt => 1.0 / x.sqrt(),
10865 Activation::Neg => -x,
10866 Activation::Abs => x.abs(),
10867 Activation::Gelu | Activation::GeluApprox => {
10868 let c = 0.797_884_6_f32;
10869 let x3 = x * x * x;
10870 let inner = (c * (x + 0.044_715 * x3)).clamp(-15.0, 15.0);
10871 0.5 * x * (1.0 + inner.tanh())
10872 }
10873 Activation::Silu => {
10874 let nx = (-x).clamp(-88.0, 88.0);
10875 x / (1.0 + nx.exp())
10876 }
10877 Activation::Round => x.round(),
10878 Activation::Sin => x.sin(),
10879 Activation::Cos => x.cos(),
10880 Activation::Tan => x.tan(),
10881 Activation::Atan => x.atan(),
10882 })
10883 .collect()
10884}
10885
10886fn collect_coop_f16_vk_mirror_activations(graph: &Graph, dev: &wgpu::Device) -> HashSet<NodeId> {
10888 let mut acts = HashSet::new();
10889 for node in graph.nodes() {
10890 if !matches!(node.op, Op::MatMul) {
10891 continue;
10892 }
10893 let a_id = node.inputs[0];
10894 let b_id = node.inputs[1];
10895 let a_shape = graph.node(a_id).shape.dims();
10896 let b_shape = graph.node(b_id).shape.dims();
10897 if a_shape.len() != 2 || b_shape.len() != 2 {
10898 continue;
10899 }
10900 let m = a_shape[0].unwrap_static() as u32;
10901 let k = a_shape[1].unwrap_static() as u32;
10902 let n = b_shape[1].unwrap_static() as u32;
10903 if !coop_f16_vk_eligible(dev, m, k, n) || !traces_to_param(graph, b_id) {
10904 continue;
10905 }
10906 if matches!(graph.node(a_id).op, Op::Activation(_)) {
10907 acts.insert(a_id);
10908 }
10909 if matches!(graph.node(b_id).op, Op::Activation(_)) {
10910 acts.insert(b_id);
10911 }
10912 }
10913 acts
10914}
10915
10916fn maybe_push_coop_f16_vk_casts(
10919 graph: &Graph,
10920 a_id: NodeId,
10921 b_id: NodeId,
10922 mirror_acts: &HashSet<NodeId>,
10923 device: &wgpu::Device,
10924 arena: &Arena,
10925 schedule: &mut Vec<Step>,
10926 uniforms: &mut Vec<wgpu::Buffer>,
10927 bind_groups: &mut Vec<wgpu::BindGroup>,
10928 mm_cast: &Option<&'static Kernel>,
10929 compute_precision: MatmulCompute,
10930 a_off_f32: u32,
10931 m: u32,
10932 k: u32,
10933 batch: u32,
10934 b_off_f32: u32,
10935 n: u32,
10936) {
10937 if compute_precision != MatmulCompute::CoopF16Vk {
10938 return;
10939 }
10940 let batch_n = batch.max(1);
10941 if !traces_to_input(graph, a_id)
10942 && !traces_to_param(graph, a_id)
10943 && !mirror_acts.contains(&a_id)
10944 {
10945 let a_elems = m.saturating_mul(k).saturating_mul(batch_n);
10946 let (base, size) = arena_window_for_nodes(device, arena, &[a_id]);
10947 push_cast_f32_to_f16_step(
10948 device,
10949 arena,
10950 base,
10951 size,
10952 schedule,
10953 uniforms,
10954 bind_groups,
10955 mm_cast,
10956 a_off_f32,
10957 a_elems,
10958 );
10959 }
10960 if !traces_to_input(graph, b_id)
10961 && !traces_to_param(graph, b_id)
10962 && !mirror_acts.contains(&b_id)
10963 {
10964 let b_elems = k.saturating_mul(n).saturating_mul(batch_n);
10965 let (base, size) = arena_window_for_nodes(device, arena, &[b_id]);
10966 push_cast_f32_to_f16_step(
10967 device,
10968 arena,
10969 base,
10970 size,
10971 schedule,
10972 uniforms,
10973 bind_groups,
10974 mm_cast,
10975 b_off_f32,
10976 b_elems,
10977 );
10978 }
10979}
10980
10981fn build_matmul_qkv_coop_f16_vk_bind_group(
10982 device: &wgpu::Device,
10983 mqk: &Kernel,
10984 arena: &Arena,
10985 arena_base: u64,
10986 arena_size: u64,
10987 params: &wgpu::Buffer,
10988 k: u32,
10989 n: u32,
10990 b_off: u32,
10991) -> (wgpu::BindGroup, u32) {
10992 let f16_buf = arena
10993 .f16_buffer
10994 .as_ref()
10995 .expect("CoopF16Vk QKV requires SHADER_F16 f16 shadow arena");
10996 let (f16_res, rebased_b) = {
10997 let (base, size, rebased) =
10998 f16_weight_bind_range(device, f16_buf.size(), b_off, k, n, 1, 0);
10999 (
11000 wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11001 buffer: f16_buf,
11002 offset: base,
11003 size: NonZeroU64::new(size),
11004 }),
11005 rebased,
11006 )
11007 };
11008 (
11009 device.create_bind_group(&wgpu::BindGroupDescriptor {
11010 label: Some("rlx-wgpu matmul_qkv_coop_f16_vk bg"),
11011 layout: &mqk.bgl,
11012 entries: &[
11013 wgpu::BindGroupEntry {
11014 binding: 0,
11015 resource: f16_res,
11016 },
11017 wgpu::BindGroupEntry {
11018 binding: 1,
11019 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11020 buffer: &arena.buffer,
11021 offset: arena_base,
11022 size: NonZeroU64::new(arena_size),
11023 }),
11024 },
11025 wgpu::BindGroupEntry {
11026 binding: 2,
11027 resource: params.as_entire_binding(),
11028 },
11029 ],
11030 }),
11031 rebased_b,
11032 )
11033}
11034fn push_cast_f32_to_f16_step(
11038 device: &wgpu::Device,
11039 arena: &Arena,
11040 arena_base: u64,
11041 arena_size: u64,
11042 schedule: &mut Vec<Step>,
11043 uniforms: &mut Vec<wgpu::Buffer>,
11044 bind_groups: &mut Vec<wgpu::BindGroup>,
11045 mm_cast: &Option<&'static Kernel>,
11046 src_off: u32,
11047 len: u32,
11048) {
11049 let kernel = match mm_cast {
11050 Some(k) => *k,
11051 None => return, };
11053 let f16_buf = match &arena.f16_buffer {
11054 Some(b) => b,
11055 None => return,
11056 };
11057 let p = CastF32ToF16Params {
11058 src_off: src_off.saturating_sub((arena_base / 4) as u32),
11059 len,
11060 _p0: 0,
11061 _p1: 0,
11062 };
11063 let u = device.create_buffer(&wgpu::BufferDescriptor {
11064 label: Some("rlx-wgpu cast_f32_to_f16 uniform"),
11065 size: std::mem::size_of::<CastF32ToF16Params>() as u64,
11066 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
11067 mapped_at_creation: false,
11068 });
11069 let dev = wgpu_device().expect("rlx-wgpu: device gone");
11071 dev.queue.write_buffer(&u, 0, bytemuck::bytes_of(&p));
11072 let (f16_base, f16_size) = f16_shadow_bind_range(arena_base, arena_size, f16_buf.size());
11073 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
11074 label: Some("rlx-wgpu cast_f32_to_f16 bg"),
11075 layout: &kernel.bgl,
11076 entries: &[
11077 wgpu::BindGroupEntry {
11078 binding: 0,
11079 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11080 buffer: f16_buf,
11081 offset: f16_base,
11082 size: NonZeroU64::new(f16_size),
11083 }),
11084 },
11085 wgpu::BindGroupEntry {
11086 binding: 1,
11087 resource: u.as_entire_binding(),
11088 },
11089 wgpu::BindGroupEntry {
11090 binding: 2,
11091 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11092 buffer: &arena.buffer,
11093 offset: arena_base,
11094 size: NonZeroU64::new(arena_size),
11095 }),
11096 },
11097 ],
11098 });
11099 schedule.push(Step::CastF32ToF16 { params: p });
11100 uniforms.push(u);
11101 bind_groups.push(bg);
11102}
11103
11104fn build_matmul_bind_group(
11108 device: &wgpu::Device,
11109 mm_k: &Kernel,
11110 _mm_w: &Kernel,
11111 mm_f16w: &Option<&'static Kernel>,
11112 mm_f16c: &Option<&'static Kernel>,
11113 mm_coop: &Option<&'static Kernel>,
11114 mm_coop_f32: &Option<&'static Kernel>,
11115 arena: &Arena,
11116 arena_base: u64,
11117 arena_size: u64,
11118 params: &wgpu::Buffer,
11119 b_is_param: bool,
11120 compute_precision: MatmulCompute,
11121 k: u32,
11122 n: u32,
11123 batch: u32,
11124 b_off: u32,
11125 b_batch_stride: u32,
11126) -> (wgpu::BindGroup, u32) {
11127 let f16_bind = |b_off: u32| -> (wgpu::BindingResource<'_>, u32) {
11128 let f16_buf = arena
11129 .f16_buffer
11130 .as_ref()
11131 .expect("f16 weight bind without f16_buffer");
11132 let (base, size, rebased) =
11133 f16_weight_bind_range(device, f16_buf.size(), b_off, k, n, batch, b_batch_stride);
11134 (
11135 wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11136 buffer: f16_buf,
11137 offset: base,
11138 size: NonZeroU64::new(size),
11139 }),
11140 rebased,
11141 )
11142 };
11143 if compute_precision == MatmulCompute::CoopF16Vk
11144 && let (Some(coop_vk), Some(_f16_buf)) =
11145 (matmul_coop_f16_vulkan_kernel(device), &arena.f16_buffer)
11146 {
11147 let (f16_res, rebased_b) = f16_bind(b_off);
11148 return (
11149 device.create_bind_group(&wgpu::BindGroupDescriptor {
11150 label: Some("rlx-wgpu matmul_coop_f16_vulkan bg"),
11151 layout: &coop_vk.bgl,
11152 entries: &[
11153 wgpu::BindGroupEntry {
11154 binding: 0,
11155 resource: f16_res,
11156 },
11157 wgpu::BindGroupEntry {
11158 binding: 1,
11159 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11160 buffer: &arena.buffer,
11161 offset: arena_base,
11162 size: NonZeroU64::new(arena_size),
11163 }),
11164 },
11165 wgpu::BindGroupEntry {
11166 binding: 2,
11167 resource: params.as_entire_binding(),
11168 },
11169 ],
11170 }),
11171 rebased_b,
11172 );
11173 }
11174 if b_is_param
11175 && compute_precision == MatmulCompute::CoopF32
11176 && let Some(coop_f32) = mm_coop_f32
11177 {
11178 return (
11181 device.create_bind_group(&wgpu::BindGroupDescriptor {
11182 label: Some("rlx-wgpu matmul_coop_f32 bg"),
11183 layout: &coop_f32.bgl,
11184 entries: &[
11185 wgpu::BindGroupEntry {
11186 binding: 0,
11187 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11188 buffer: &arena.buffer,
11189 offset: arena_base,
11190 size: NonZeroU64::new(arena_size),
11191 }),
11192 },
11193 wgpu::BindGroupEntry {
11194 binding: 1,
11195 resource: params.as_entire_binding(),
11196 },
11197 ],
11198 }),
11199 b_off,
11200 );
11201 }
11202 if b_is_param
11203 && compute_precision == MatmulCompute::Coop16
11204 && let (Some(_f16_buf), Some(coop)) = (&arena.f16_buffer, mm_coop)
11205 {
11206 let (f16_res, rebased_b) = f16_bind(b_off);
11207 return (
11211 device.create_bind_group(&wgpu::BindGroupDescriptor {
11212 label: Some("rlx-wgpu matmul_coop16 bg"),
11213 layout: &coop.bgl,
11214 entries: &[
11215 wgpu::BindGroupEntry {
11216 binding: 0,
11217 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11218 buffer: &arena.buffer,
11219 offset: arena_base,
11220 size: NonZeroU64::new(arena_size),
11221 }),
11222 },
11223 wgpu::BindGroupEntry {
11224 binding: 1,
11225 resource: params.as_entire_binding(),
11226 },
11227 wgpu::BindGroupEntry {
11228 binding: 2,
11229 resource: f16_res,
11230 }, ],
11232 }),
11233 rebased_b,
11234 );
11235 }
11236 if b_is_param
11237 && compute_precision == MatmulCompute::F16
11238 && let (Some(_f16_buf), Some(f16c)) = (&arena.f16_buffer, mm_f16c)
11239 {
11240 let (f16_res, rebased_b) = f16_bind(b_off);
11241 return (
11242 device.create_bind_group(&wgpu::BindGroupDescriptor {
11243 label: Some("rlx-wgpu matmul_f16_compute bg"),
11244 layout: &f16c.bgl,
11245 entries: &[
11246 wgpu::BindGroupEntry {
11247 binding: 0,
11248 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11249 buffer: &arena.buffer,
11250 offset: arena_base,
11251 size: NonZeroU64::new(arena_size),
11252 }),
11253 },
11254 wgpu::BindGroupEntry {
11255 binding: 1,
11256 resource: params.as_entire_binding(),
11257 },
11258 wgpu::BindGroupEntry {
11259 binding: 2,
11260 resource: f16_res,
11261 },
11262 ],
11263 }),
11264 rebased_b,
11265 );
11266 }
11267 let f16w_opt_in = rlx_ir::env::flag("RLX_WGPU_F16_WEIGHTS");
11268 if b_is_param
11269 && f16w_opt_in
11270 && let (Some(_f16_buf), Some(f16w)) = (&arena.f16_buffer, mm_f16w)
11271 {
11272 let (f16_res, rebased_b) = f16_bind(b_off);
11273 return (
11274 device.create_bind_group(&wgpu::BindGroupDescriptor {
11275 label: Some("rlx-wgpu matmul_f16w bg"),
11276 layout: &f16w.bgl,
11277 entries: &[
11278 wgpu::BindGroupEntry {
11279 binding: 0,
11280 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11281 buffer: &arena.buffer,
11282 offset: arena_base,
11283 size: NonZeroU64::new(arena_size),
11284 }),
11285 },
11286 wgpu::BindGroupEntry {
11287 binding: 1,
11288 resource: params.as_entire_binding(),
11289 },
11290 wgpu::BindGroupEntry {
11291 binding: 2,
11292 resource: f16_res,
11293 },
11294 ],
11295 }),
11296 rebased_b,
11297 );
11298 }
11299 (
11300 bind_two_buf0_window(device, mm_k, &arena.buffer, arena_base, arena_size, params),
11301 b_off,
11302 )
11303}