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