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