1#![allow(unused_imports)]
19
20use crate::buffer::{
21 Arena, ReadbackLayout, ReadbackStaging, TinyReadbackStaging, decode_mapped_readback_f32,
22 decode_tiny_mapped_f32, encode_readback_copies, plan_f32_uniform, read_f32_many_pooled,
23 schedule_readback_map, use_tiny_readback, wait_readback_map,
24};
25use crate::device::wgpu_device;
26use crate::kernels::{
27 ArgmaxParams, AttentionBwdParams, AttentionParams, BatchElementwiseRegionParams, BinaryParams,
28 Conv1dParams, Conv2dParams, Conv3dParams, CopyParams, CumsumBwdParams, CumsumParams,
29 DequantMatmulParams, ElementwiseRegionParams, ExpandParams, FmaParams, FusedResidualLnParams,
30 FusedResidualLnTeeParams, FusedResidualRmsNormParams, GatherAxisParams, GatherBwdParams,
31 GatherParams, GroupedMatmulParams, GruParams, Kernel, LayerNormBwdParams, LayerNormParams,
32 Mamba2Params, MatmulParams, MatmulQkvParams, NarrowConcatParams, Pool1dParams, Pool2dParams,
33 Pool3dParams, ReduceParams, RmsNormBwdParams, RnnParams, RopeBwdParams, RopeParams,
34 SampleParams, ScatterAddParams, SceParams, SelectiveScanParams, SoftmaxParams, TopKParams,
35 TransposeParams, UmapKnnParams, UnaryParams, WelchPeaksGpuParams, WhereParams, argmax_kernel,
36 attention_bwd_kernel, attention_kernel, batch_elementwise_region_kernel, binary_kernel,
37 cast_f32_to_f16_kernel, compare_kernel, concat_kernel, conv1d_kernel, conv2d_kernel,
38 conv3d_kernel, copy_kernel, cumsum_backward_kernel, cumsum_kernel, dequant_matmul_kernel,
39 elementwise_region_kernel, elementwise_region_spatial_kernel, expand_kernel, fma_kernel,
40 fused_residual_ln_kernel, fused_residual_ln_tee_kernel, fused_residual_rms_norm_kernel,
41 gather_axis_kernel, gather_backward_acc_kernel, gather_backward_zero_kernel, gather_kernel,
42 gather_split_kernel, grouped_matmul_kernel, gru_kernel,
43 layer_norm_backward_gamma_partial_kernel, layer_norm_backward_gamma_reduce_kernel,
44 layer_norm_backward_input_kernel, layernorm_kernel, mamba2_kernel,
45 matmul_coop_f16_vulkan_active_kernel, matmul_coop_f16_vulkan_kernel,
46 matmul_coop_f32_active_kernel, matmul_coop16_kernel, matmul_f16_compute_kernel,
47 matmul_f16w_kernel, matmul_kernel, matmul_qkv_coop_f16_vk_active_kernel,
48 matmul_qkv_coop_f16_vk_kernel, matmul_qkv_coop_f32_kernel, matmul_qkv_kernel,
49 matmul_wide_active_kernel, matmul_wide_kernel, narrow_kernel, pool1d_kernel, pool2d_kernel,
50 pool3d_kernel, reduce_kernel, rms_norm_backward_kernel, rms_norm_backward_param_kernel,
51 rnn_kernel, rope_backward_kernel, rope_kernel, sample_kernel, scatter_add_kernel,
52 selective_scan_kernel, softmax_cross_entropy_kernel, softmax_kernel, topk_kernel,
53 transpose_kernel, umap_knn_kernel, unary_f16_mirror_kernel, unary_kernel,
54 welch_peaks_gpu_kernel, where_kernel,
55};
56use rlx_ir::dynamic::{bind_graph, has_dynamic_dims, infer_bindings_from_f32_inputs, same_binding};
57use rlx_ir::op::{Activation, BinaryOp, CmpOp, MaskKind, ReduceOp};
58use rlx_ir::shape::DimBinding;
59use rlx_ir::{Graph, NodeId, Op};
60use std::collections::{HashMap, HashSet};
61use std::num::NonZeroU64;
62
63use super::*;
64
65impl WgpuExecutable {
66 pub fn compile_with_bindings(graph: Graph, bindings: &DimBinding) -> Self {
72 if bindings.is_empty() {
73 return Self::compile(graph);
74 }
75 let mut fresh = Graph::new(&graph.name);
77 for node in graph.nodes() {
78 let bound = node.shape.bind(bindings);
79 fresh.add_node(node.op.clone(), node.inputs.clone(), bound);
80 }
81 fresh.set_outputs(graph.outputs.clone());
82 Self::compile(fresh)
83 }
84
85 pub fn compile(graph: Graph) -> Self {
86 Self::compile_rng(graph, rlx_ir::RngOptions::default())
87 }
88
89 pub fn compile_rng(graph: Graph, rng: rlx_ir::RngOptions) -> Self {
90 let rng = std::sync::Arc::new(std::sync::RwLock::new(rng));
91 if has_dynamic_dims(&graph) {
92 return Self::deferred(graph, rng);
93 }
94 Self::compile_static_inner(graph, rng)
95 }
96
97 pub(crate) fn compile_static_inner(
98 graph: Graph,
99 rng: std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
100 ) -> Self {
101 let dev = wgpu_device().expect("rlx-wgpu: no compatible adapter found");
102
103 let graph = crate::unfuse::unfuse(graph);
110
111 let mut plan = plan_f32_uniform(&graph, 16);
113 let dequant_scratch = crate::gguf_gpu::dequant_gguf_scratch_bytes(&graph);
114 let dequant_scratch_off = if dequant_scratch > 0 {
115 let aligned = plan.arena_size.div_ceil(16) * 16;
116 let new_size = aligned + dequant_scratch.div_ceil(16) * 16;
117 if (new_size as u64) <= dev.device.limits().max_buffer_size {
118 plan.arena_size = new_size;
119 aligned
120 } else {
121 0
122 }
123 } else {
124 0
125 };
126 let scratch_bytes = compute_scratch_bytes(&graph);
130 let mut arena = Arena::from_plan_with_scratch(&dev.device, &plan, scratch_bytes);
131 for node in graph.nodes() {
135 let elems = node.shape.num_elements().unwrap_or(0);
136 arena.set_actual_len(node.id, elems * 4);
137 }
138
139 for node in graph.nodes() {
145 if let Op::Constant { data } = &node.op
146 && arena.has(node.id)
147 && !data.is_empty()
148 {
149 let widened: Option<Vec<u8>> = match node.shape.dtype() {
150 rlx_ir::DType::I64 => Some(
151 data.chunks_exact(8)
152 .flat_map(|c| {
153 (i64::from_le_bytes(c.try_into().unwrap()) as f32).to_le_bytes()
154 })
155 .collect(),
156 ),
157 rlx_ir::DType::I32 => Some(
158 data.chunks_exact(4)
159 .flat_map(|c| {
160 (i32::from_le_bytes(c.try_into().unwrap()) as f32).to_le_bytes()
161 })
162 .collect(),
163 ),
164 rlx_ir::DType::U32 => Some(
165 data.chunks_exact(4)
166 .flat_map(|c| {
167 (u32::from_le_bytes(c.try_into().unwrap()) as f32).to_le_bytes()
168 })
169 .collect(),
170 ),
171 rlx_ir::DType::Bool | rlx_ir::DType::U8 => Some(
172 data.iter()
173 .flat_map(|&b| (b as f32).to_le_bytes())
174 .collect(),
175 ),
176 rlx_ir::DType::I8 => Some(
177 data.iter()
178 .flat_map(|&b| ((b as i8) as f32).to_le_bytes())
179 .collect(),
180 ),
181 _ => None,
182 };
183 let bytes: &[u8] = widened.as_deref().unwrap_or(data);
184 let bytes_to_write = bytes.len().min(arena.len_of(node.id));
185 dev.queue.write_buffer(
186 &arena.buffer,
187 arena.offset(node.id) as u64,
188 &bytes[..bytes_to_write],
189 );
190 }
191 }
192
193 let mut input_offsets = HashMap::new();
194 let mut param_offsets = HashMap::new();
195 for node in graph.nodes() {
196 match &node.op {
197 Op::Input { name } => {
198 input_offsets.insert(name.clone(), node.id);
199 }
200 Op::Param { name } => {
201 param_offsets.insert(name.clone(), node.id);
202 }
203 _ => {}
204 }
205 }
206
207 let mm_k = matmul_kernel(&dev.device);
208 let mm_w = matmul_wide_kernel(&dev.device);
209 let _mm_w_active = matmul_wide_active_kernel(&dev.device);
210 let mm_f16w = matmul_f16w_kernel(&dev.device);
211 let mm_f16c = matmul_f16_compute_kernel(&dev.device);
212 let mm_coop = matmul_coop16_kernel(&dev.device);
213 let mm_coop_f32 = matmul_coop_f32_active_kernel(&dev.device);
214 let mm_cast = cast_f32_to_f16_kernel(&dev.device);
215 let bk = binary_kernel(&dev.device);
216 let uk = unary_kernel(&dev.device);
217 let ck = compare_kernel(&dev.device);
218 let wk = where_kernel(&dev.device);
219 let fk = fma_kernel(&dev.device);
220
221 let mut schedule = Vec::new();
222 let mut uniforms = Vec::new();
223 let mut bind_groups = Vec::new();
224 let mut fft_gpu_steps: Vec<crate::fft_dispatch::FftGpuResources> = Vec::new();
225 let mut gguf_host_pad: Option<(wgpu::Buffer, wgpu::BindGroup)> = None;
226 let mut meta_buffers: Vec<wgpu::Buffer> = Vec::new();
227 let mut coop_f16_b_param: HashMap<u32, String> = HashMap::new();
228 let mut coop_f16_vk_wide_bind_groups: HashMap<usize, wgpu::BindGroup> = HashMap::new();
229 let mm_w_active_compile = matmul_wide_active_kernel(&dev.device);
230
231 let coop_f16_vk_mirror_acts = collect_coop_f16_vk_mirror_activations(&graph, &dev.device);
232
233 let mut qkv_split: HashMap<NodeId, (NodeId, NodeId, NodeId)> = HashMap::new();
246 for (parent_id, qkv) in detect_split_qkv_pattern(&graph) {
247 let parent = graph.node(parent_id);
248 let a_id = parent.inputs[0];
251 let b_id = parent.inputs[1];
252 let a_dims = graph.node(a_id).shape.dims();
253 let b_dims = graph.node(b_id).shape.dims();
254 let out_dims = parent.shape.dims();
255 let (m, k, n) =
256 if a_dims.len() >= 2 && b_dims.len() == 2 && out_dims.len() == a_dims.len() {
257 let leading: usize = a_dims[..a_dims.len() - 2]
258 .iter()
259 .map(|d| d.unwrap_static())
260 .product();
261 let m_inner = a_dims[a_dims.len() - 2].unwrap_static();
262 let k_inner = a_dims[a_dims.len() - 1].unwrap_static();
263 let n_inner = b_dims[1].unwrap_static();
264 ((leading * m_inner) as u32, k_inner as u32, n_inner as u32)
265 } else if a_dims.len() == 2 && b_dims.len() == 2 {
266 (
267 a_dims[0].unwrap_static() as u32,
268 a_dims[1].unwrap_static() as u32,
269 b_dims[1].unwrap_static() as u32,
270 )
271 } else {
272 continue; };
274 let cp = derive_matmul_compute(
275 &dev.device,
276 &graph,
277 &coop_f16_vk_mirror_acts,
278 a_id,
279 b_id,
280 m,
281 k,
282 n,
283 );
284 if cp == MatmulCompute::F32 || cp == MatmulCompute::CoopF32 {
289 qkv_split.insert(parent_id, qkv);
290 }
291 }
292 let qkv_skip_narrows: HashSet<NodeId> = qkv_split
293 .values()
294 .flat_map(|&(q, k, v)| [q, k, v])
295 .collect();
296
297 let mut packed_bshd_attn: HashMap<NodeId, (NodeId, u32)> = HashMap::new();
301 let mut packed_bshd_skip_narrows: HashSet<NodeId> = HashSet::new();
302 if !rlx_ir::env::flag("RLX_WGPU_NO_PACKED_BSHD_ATTN") {
303 for node in graph.nodes() {
304 let Op::Attention { .. } = &node.op else {
305 continue;
306 };
307 if node.inputs.len() < 3 {
308 continue;
309 }
310 if let Some((parent, head_width, narrows)) =
311 rlx_ir::detect_packed_bshd_qkv_attention(
312 &graph,
313 node.inputs[0],
314 node.inputs[1],
315 node.inputs[2],
316 )
317 {
318 packed_bshd_attn.insert(node.id, (parent, head_width as u32));
319 for narrow in narrows {
320 if rlx_ir::packed_bshd_narrow_elidable(&graph, narrow, node.id) {
321 packed_bshd_skip_narrows.insert(narrow);
322 }
323 }
324 }
325 }
326 }
327
328 let (ln_to_tee, skip_adds) = detect_residual_ln_tee_pattern(&graph);
338
339 let mut coop_f16_host_activations: Vec<(NodeId, Activation, String)> = Vec::new();
340
341 let emit_uniform = |size: usize| -> wgpu::Buffer {
342 dev.device.create_buffer(&wgpu::BufferDescriptor {
343 label: Some("rlx-wgpu uniform"),
344 size: size as u64,
345 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
346 mapped_at_creation: false,
347 })
348 };
349
350 for node in graph.nodes() {
351 let elems = node.shape.num_elements().unwrap_or(0) as u32;
355 match &node.op {
356 Op::Input { .. } | Op::Param { .. } | Op::Constant { .. } => continue,
357 Op::MatMul => {
358 let a_id = node.inputs[0];
359 let b_id = node.inputs[1];
360 let a_shape = graph.node(a_id).shape.dims();
361 let b_shape = graph.node(b_id).shape.dims();
362 let out_shape = node.shape.dims();
363 let (m, k, n, batch, a_bs, b_bs, c_bs) = if a_shape.len() == 2
368 && b_shape.len() == 2
369 && out_shape.len() == 2
370 {
371 (
372 a_shape[0].unwrap_static() as u32,
373 a_shape[1].unwrap_static() as u32,
374 b_shape[1].unwrap_static() as u32,
375 1u32,
376 0u32,
377 0u32,
378 0u32,
379 )
380 } else if a_shape.len() >= 2
381 && b_shape.len() == 2
382 && out_shape.len() == a_shape.len()
383 {
384 let leading: usize = a_shape[..a_shape.len() - 2]
385 .iter()
386 .map(|d| d.unwrap_static())
387 .product();
388 let m_inner = a_shape[a_shape.len() - 2].unwrap_static();
389 let k_inner = a_shape[a_shape.len() - 1].unwrap_static();
390 let n_inner = b_shape[1].unwrap_static();
391 (
392 (leading * m_inner) as u32,
393 k_inner as u32,
394 n_inner as u32,
395 1u32,
396 0u32,
397 0u32,
398 0u32,
399 )
400 } else if (a_shape.len() >= 3 || b_shape.len() >= 3)
401 && a_shape.len() >= 2
402 && b_shape.len() >= 2
403 && out_shape.len() == a_shape.len().max(b_shape.len())
404 {
405 let lead_a: usize = a_shape[..a_shape.len() - 2]
409 .iter()
410 .map(|d| d.unwrap_static())
411 .product();
412 let lead_b: usize = b_shape[..b_shape.len() - 2]
413 .iter()
414 .map(|d| d.unwrap_static())
415 .product();
416 if lead_a != lead_b {
417 panic!(
418 "rlx-wgpu MatMul: batched leading-product mismatch \
419 a={a_shape:?} b={b_shape:?} out={out_shape:?}"
420 );
421 }
422 let b_count: usize = lead_a;
423 let m_inner = a_shape[a_shape.len() - 2].unwrap_static();
424 let k_inner = a_shape[a_shape.len() - 1].unwrap_static();
425 let n_inner = b_shape[b_shape.len() - 1].unwrap_static();
426 (
427 m_inner as u32,
428 k_inner as u32,
429 n_inner as u32,
430 b_count as u32,
431 (m_inner * k_inner) as u32,
432 (k_inner * n_inner) as u32,
433 (m_inner * n_inner) as u32,
434 )
435 } else {
436 panic!(
437 "rlx-wgpu MatMul: unsupported shapes a={a_shape:?} b={b_shape:?} \
438 out={out_shape:?} (supported: 2D×2D, [..,M,K]×[K,N], [..,M,K]×[..,K,N])"
439 );
440 };
441 let b_is_param = tensor_is_graph_param(&graph, ¶m_offsets, b_id);
442 let b_bytes = arena.len_of(b_id) as u64;
443 let mut compute_precision = derive_matmul_compute(
444 &dev.device,
445 &graph,
446 &coop_f16_vk_mirror_acts,
447 a_id,
448 b_id,
449 m,
450 k,
451 n,
452 );
453 if b_is_param
454 && b_bytes > ARENA_STAGE_CAP
455 && arena.param_fits_f16_mirror(b_id)
456 && !rlx_ir::env::flag("RLX_WGPU_NO_F16_MIRROR")
457 {
458 compute_precision = MatmulCompute::F16;
459 }
460 let b_in_arena = !matmul_b_from_f16(compute_precision, b_is_param);
461 let (mut base, mut size, param_anchor) = arena_matmul_bind_window(
462 &dev.device,
463 &arena,
464 &graph,
465 ¶m_offsets,
466 node.id,
467 a_id,
468 b_id,
469 b_in_arena,
470 );
471 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
472 let expand_ids: &[NodeId] = if b_in_arena {
474 &[node.id, a_id, b_id]
475 } else {
476 &[node.id, a_id]
477 };
478 arena_expand_bind_window(&arena, expand_ids, &mut base, &mut size, max_binding);
479 let mut scratch = arena.scratch_off as u64;
480 if param_anchor {
481 arena_ensure_scratch_in_window(&mut scratch, base, size);
482 }
483 if b_is_param && b_bytes > ARENA_STAGE_CAP && b_in_arena {
484 assert!(
493 arena_tensor_in_window(&arena, b_id, base, size),
494 "rlx-wgpu matmul: large param B {:?} off={} not in window base={base} size={size}",
495 b_id,
496 arena.offset(b_id),
497 );
498 }
499 let a_off_f32 = arena_off_in_bind_window(
500 &graph,
501 ¶m_offsets,
502 &dev.device,
503 &arena,
504 &mut schedule,
505 &mut scratch,
506 a_id,
507 &mut base,
508 &mut size,
509 );
510 let b_off_f32 = if !b_in_arena {
511 (arena.offset(b_id) / 4) as u32
516 } else if b_is_param
517 && b_bytes > ARENA_STAGE_CAP
518 && arena_tensor_in_window(&arena, b_id, base, size)
519 {
520 arena_local_off_f32(&arena, b_id, base)
521 } else {
522 arena_off_in_bind_window(
523 &graph,
524 ¶m_offsets,
525 &dev.device,
526 &arena,
527 &mut schedule,
528 &mut scratch,
529 b_id,
530 &mut base,
531 &mut size,
532 )
533 };
534 maybe_push_coop_f16_vk_casts(
535 &graph,
536 a_id,
537 b_id,
538 &coop_f16_vk_mirror_acts,
539 &dev.device,
540 &arena,
541 &mut schedule,
542 &mut uniforms,
543 &mut bind_groups,
544 &mm_cast,
545 compute_precision,
546 a_off_f32,
547 m,
548 k,
549 batch,
550 b_off_f32,
551 n,
552 );
553 schedule.push(Step::Matmul {
554 m,
555 k,
556 n,
557 batch,
558 a_batch_stride: a_bs,
559 b_batch_stride: b_bs,
560 c_batch_stride: c_bs,
561 a_off_f32,
562 b_off_f32,
563 c_off_f32: arena_local_off_f32(&arena, node.id, base),
564 has_bias: 0,
565 bias_off_f32: 0,
566 act_id: 0xFFFF,
567 b_is_param,
568 compute_precision,
569 });
570 let b_off_global = (arena.offset(b_id) / 4) as u32;
571 let b_off_bind = if b_is_param
572 && matches!(
573 compute_precision,
574 MatmulCompute::Coop16 | MatmulCompute::CoopF16Vk | MatmulCompute::F16
575 ) {
576 b_off_global
577 } else {
578 b_off_f32
579 };
580 register_coop_f16_vk_b_param(
581 &mut coop_f16_b_param,
582 ¶m_offsets,
583 b_id,
584 b_off_bind,
585 compute_precision,
586 );
587 let u = emit_uniform(std::mem::size_of::<MatmulParams>());
588 let (bg, b_off_adj) = build_matmul_bind_group(
589 &dev.device,
590 mm_k,
591 mm_w,
592 &mm_f16w,
593 &mm_f16c,
594 &mm_coop,
595 &mm_coop_f32,
596 &arena,
597 base,
598 size,
599 &u,
600 b_is_param,
601 compute_precision,
602 k,
603 n,
604 batch,
605 b_off_bind,
606 b_bs,
607 );
608 if let Some(Step::Matmul { b_off_f32, .. }) = schedule.last_mut() {
609 *b_off_f32 = b_off_adj;
610 }
611 uniforms.push(u);
612 bind_groups.push(bg);
613 if compute_precision == MatmulCompute::CoopF16Vk {
614 coop_f16_vk_wide_bind_groups.insert(
615 schedule.len() - 1,
616 bind_two_buf0_window(
617 &dev.device,
618 mm_w_active_compile,
619 &arena.buffer,
620 base,
621 size,
622 &uniforms[uniforms.len() - 1],
623 ),
624 );
625 }
626 }
627 Op::Binary(bop) => {
628 if skip_adds.contains(&node.id) {
633 continue;
634 }
635 require_equal_shapes(&graph, &node.inputs, "Binary");
636 let a_id = node.inputs[0];
637 let b_id = node.inputs[1];
638 let win_ids = [node.id, a_id, b_id];
639 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
640 let fits = arena_span_bytes(&arena, &win_ids) <= max_binding;
641 let mut scratch = arena.scratch_off as u64;
642 let (mut base, mut size, param_anchor) = arena_multi_op_window(
643 &dev.device,
644 &arena,
645 &graph,
646 ¶m_offsets,
647 &mut schedule,
648 &mut scratch,
649 &win_ids,
650 );
651 if !fits && !param_anchor {
652 base = arena_bind_window_covering_scratch_if_needed(
653 &arena, base, size, scratch,
654 );
655 }
656 let a_off = arena_off_in_bind_window(
657 &graph,
658 ¶m_offsets,
659 &dev.device,
660 &arena,
661 &mut schedule,
662 &mut scratch,
663 a_id,
664 &mut base,
665 &mut size,
666 );
667 let b_off = arena_off_in_bind_window(
668 &graph,
669 ¶m_offsets,
670 &dev.device,
671 &arena,
672 &mut schedule,
673 &mut scratch,
674 b_id,
675 &mut base,
676 &mut size,
677 );
678 let p = BinaryParams {
679 n: elems,
680 a_off,
681 b_off,
682 c_off: arena_local_off_f32(&arena, node.id, base),
683 op: binary_op_id(*bop),
684 _p0: 0,
685 _p1: 0,
686 _p2: 0,
687 };
688 schedule.push(Step::Binary { params: p });
689 let u = emit_uniform(std::mem::size_of::<BinaryParams>());
690 let bg = bind_two_buf0_window(&dev.device, bk, &arena.buffer, base, size, &u);
691 uniforms.push(u);
692 bind_groups.push(bg);
693 }
694 Op::Compare(cop) => {
695 require_equal_shapes(&graph, &node.inputs, "Compare");
696 let (mut base, size) = arena_window_for_nodes(&dev.device, &arena, &[node.id]);
697 let a_id = node.inputs[0];
698 let b_id = node.inputs[1];
699 let a_src = arena.offset(a_id) as u64;
700 let b_src = arena.offset(b_id) as u64;
701 let a_len = arena.len_of(a_id) as u64;
702 let b_len = arena.len_of(b_id) as u64;
703 let a_in = a_src >= base && a_src + a_len <= base + size;
704 let b_in = b_src >= base && b_src + b_len <= base + size;
705 let a_dst = arena.scratch_off as u64;
706 let a_aligned = a_len.div_ceil(256) * 256;
707 let b_dst = a_dst + a_aligned;
708 if a_dst < base || b_dst + b_len > base + size {
709 base = (arena.size as u64).saturating_sub(size);
710 base = (base / 256) * 256;
711 }
712 let a_off = if a_in {
713 arena_local_off_f32(&arena, a_id, base)
714 } else {
715 if a_len > 64 * 1024 * 1024 {
716 panic!("rlx-wgpu: Compare staging operand A too large ({a_len} bytes)");
717 }
718 schedule.push(Step::BufferCopy {
719 src_byte_off: a_src,
720 dst_byte_off: a_dst,
721 bytes: a_len as u32,
722 });
723 ((a_dst.saturating_sub(base)) / 4) as u32
724 };
725 let b_off = if b_in {
726 arena_local_off_f32(&arena, b_id, base)
727 } else {
728 if b_len > 64 * 1024 * 1024 {
729 panic!("rlx-wgpu: Compare staging operand B too large ({b_len} bytes)");
730 }
731 schedule.push(Step::BufferCopy {
732 src_byte_off: b_src,
733 dst_byte_off: b_dst,
734 bytes: b_len as u32,
735 });
736 ((b_dst.saturating_sub(base)) / 4) as u32
737 };
738 let p = BinaryParams {
739 n: elems,
740 a_off,
741 b_off,
742 c_off: arena_local_off_f32(&arena, node.id, base),
743 op: compare_op_id(*cop),
744 _p0: 0,
745 _p1: 0,
746 _p2: 0,
747 };
748 schedule.push(Step::Compare { params: p });
749 let u = emit_uniform(std::mem::size_of::<BinaryParams>());
750 let bg = bind_two_buf0_window(&dev.device, ck, &arena.buffer, base, size, &u);
751 uniforms.push(u);
752 bind_groups.push(bg);
753 }
754 Op::Activation(act) => {
755 if coop_f16_vk_mirror_acts.contains(&node.id) {
756 let src_name =
757 tensor_host_name(&input_offsets, ¶m_offsets, node.inputs[0]);
758 coop_f16_host_activations.push((node.id, *act, src_name));
759 continue;
760 }
761 let in_id = node.inputs[0];
762 let win_ids = [node.id, in_id];
763 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
764 let fits = arena_span_bytes(&arena, &win_ids) <= max_binding;
765 let mut scratch = arena.scratch_off as u64;
766 let (mut base, mut size, param_anchor) = arena_multi_op_window(
767 &dev.device,
768 &arena,
769 &graph,
770 ¶m_offsets,
771 &mut schedule,
772 &mut scratch,
773 &win_ids,
774 );
775 if !fits && !param_anchor {
776 base = arena_bind_window_covering_scratch_if_needed(
777 &arena, base, size, scratch,
778 );
779 }
780 let in_off = arena_off_in_bind_window(
781 &graph,
782 ¶m_offsets,
783 &dev.device,
784 &arena,
785 &mut schedule,
786 &mut scratch,
787 in_id,
788 &mut base,
789 &mut size,
790 );
791 let p = UnaryParams {
792 n: elems,
793 in_off,
794 out_off: arena_local_off_f32(&arena, node.id, base),
795 op: activation_op_id(*act),
796 _p0: 0,
797 _p1: 0,
798 _p2: 0,
799 _p3: 0,
800 };
801 schedule.push(Step::Unary {
802 params: p,
803 f16_mirror: false,
804 });
805 let u = emit_uniform(std::mem::size_of::<UnaryParams>());
806 let bg = bind_two_buf0_window(&dev.device, uk, &arena.buffer, base, size, &u);
807 uniforms.push(u);
808 bind_groups.push(bg);
809 }
810 Op::Where => {
811 let (mut base, size) = arena_window_for_nodes(&dev.device, &arena, &[node.id]);
812 let cond_id = node.inputs[0];
813 let x_id = node.inputs[1];
814 let y_id = node.inputs[2];
815 let cond_src = arena.offset(cond_id) as u64;
816 let x_src = arena.offset(x_id) as u64;
817 let y_src = arena.offset(y_id) as u64;
818 let cond_len = arena.len_of(cond_id) as u64;
819 let x_len = arena.len_of(x_id) as u64;
820 let y_len = arena.len_of(y_id) as u64;
821 let cond_in = cond_src >= base && cond_src + cond_len <= base + size;
822 let x_in = x_src >= base && x_src + x_len <= base + size;
823 let y_in = y_src >= base && y_src + y_len <= base + size;
824 let cond_dst = arena.scratch_off as u64;
825 let cond_aligned = cond_len.div_ceil(256) * 256;
826 let x_dst = cond_dst + cond_aligned;
827 let x_aligned = x_len.div_ceil(256) * 256;
828 let y_dst = x_dst + x_aligned;
829 if cond_dst < base || y_dst + y_len > base + size {
830 base = (arena.size as u64).saturating_sub(size);
831 base = (base / 256) * 256;
832 }
833 let cond_off = if cond_in {
834 arena_local_off_f32(&arena, cond_id, base)
835 } else {
836 if cond_len > 64 * 1024 * 1024 {
837 panic!("rlx-wgpu: Where staging cond too large ({cond_len} bytes)");
838 }
839 schedule.push(Step::BufferCopy {
840 src_byte_off: cond_src,
841 dst_byte_off: cond_dst,
842 bytes: cond_len as u32,
843 });
844 ((cond_dst.saturating_sub(base)) / 4) as u32
845 };
846 let x_off = if x_in {
847 arena_local_off_f32(&arena, x_id, base)
848 } else {
849 if x_len > 64 * 1024 * 1024 {
850 panic!("rlx-wgpu: Where staging x too large ({x_len} bytes)");
851 }
852 schedule.push(Step::BufferCopy {
853 src_byte_off: x_src,
854 dst_byte_off: x_dst,
855 bytes: x_len as u32,
856 });
857 ((x_dst.saturating_sub(base)) / 4) as u32
858 };
859 let y_off = if y_in {
860 arena_local_off_f32(&arena, y_id, base)
861 } else {
862 if y_len > 64 * 1024 * 1024 {
863 panic!("rlx-wgpu: Where staging y too large ({y_len} bytes)");
864 }
865 schedule.push(Step::BufferCopy {
866 src_byte_off: y_src,
867 dst_byte_off: y_dst,
868 bytes: y_len as u32,
869 });
870 ((y_dst.saturating_sub(base)) / 4) as u32
871 };
872 let p = WhereParams {
873 n: elems,
874 cond_off,
875 x_off,
876 y_off,
877 out_off: arena_local_off_f32(&arena, node.id, base),
878 _p0: 0,
879 _p1: 0,
880 _p2: 0,
881 };
882 schedule.push(Step::Where { params: p });
883 let u = emit_uniform(std::mem::size_of::<WhereParams>());
884 let bg = bind_two_buf0_window(&dev.device, wk, &arena.buffer, base, size, &u);
885 uniforms.push(u);
886 bind_groups.push(bg);
887 }
888
889 Op::Fma => {
890 let (mut base, size) = arena_window_for_nodes(&dev.device, &arena, &[node.id]);
891 let a_id = node.inputs[0];
892 let b_id = node.inputs[1];
893 let c_id = node.inputs[2];
894 let a_src = arena.offset(a_id) as u64;
895 let b_src = arena.offset(b_id) as u64;
896 let c_src = arena.offset(c_id) as u64;
897 let a_len = arena.len_of(a_id) as u64;
898 let b_len = arena.len_of(b_id) as u64;
899 let c_len = arena.len_of(c_id) as u64;
900 let a_in = a_src >= base && a_src + a_len <= base + size;
901 let b_in = b_src >= base && b_src + b_len <= base + size;
902 let c_in = c_src >= base && c_src + c_len <= base + size;
903 let a_dst = arena.scratch_off as u64;
904 let a_aligned = a_len.div_ceil(256) * 256;
905 let b_dst = a_dst + a_aligned;
906 let b_aligned = b_len.div_ceil(256) * 256;
907 let c_dst = b_dst + b_aligned;
908 if a_dst < base || c_dst + c_len > base + size {
909 base = (arena.size as u64).saturating_sub(size);
910 base = (base / 256) * 256;
911 }
912 let a_off = if a_in {
913 arena_local_off_f32(&arena, a_id, base)
914 } else {
915 if a_len > 64 * 1024 * 1024 {
916 panic!("rlx-wgpu: Fma staging a too large ({a_len} bytes)");
917 }
918 schedule.push(Step::BufferCopy {
919 src_byte_off: a_src,
920 dst_byte_off: a_dst,
921 bytes: a_len as u32,
922 });
923 ((a_dst.saturating_sub(base)) / 4) as u32
924 };
925 let b_off = if b_in {
926 arena_local_off_f32(&arena, b_id, base)
927 } else {
928 if b_len > 64 * 1024 * 1024 {
929 panic!("rlx-wgpu: Fma staging b too large ({b_len} bytes)");
930 }
931 schedule.push(Step::BufferCopy {
932 src_byte_off: b_src,
933 dst_byte_off: b_dst,
934 bytes: b_len as u32,
935 });
936 ((b_dst.saturating_sub(base)) / 4) as u32
937 };
938 let c_off = if c_in {
939 arena_local_off_f32(&arena, c_id, base)
940 } else {
941 if c_len > 64 * 1024 * 1024 {
942 panic!("rlx-wgpu: Fma staging c too large ({c_len} bytes)");
943 }
944 schedule.push(Step::BufferCopy {
945 src_byte_off: c_src,
946 dst_byte_off: c_dst,
947 bytes: c_len as u32,
948 });
949 ((c_dst.saturating_sub(base)) / 4) as u32
950 };
951 let p = FmaParams {
952 n: elems,
953 a_off,
954 b_off,
955 c_off,
956 out_off: arena_local_off_f32(&arena, node.id, base),
957 _p0: 0,
958 _p1: 0,
959 _p2: 0,
960 };
961 schedule.push(Step::Fma { params: p });
962 let u = emit_uniform(std::mem::size_of::<FmaParams>());
963 let bg = bind_two_buf0_window(&dev.device, fk, &arena.buffer, base, size, &u);
964 uniforms.push(u);
965 bind_groups.push(bg);
966 }
967
968 Op::BatchElementwiseRegion {
969 chain,
970 num_batch_inputs,
971 scalar_input_mask,
972 input_modulus,
973 prologue,
974 prologue_input,
975 } => {
976 let n = *num_batch_inputs as usize;
977 if n == 0 || chain.len() > 32 {
978 panic!(
979 "rlx-wgpu BatchElementwiseRegion: num_batch_inputs={n} steps={}",
980 chain.len()
981 );
982 }
983 let slice_shape = rlx_ir::batch_region_slice_shape(&node.shape);
984 let slice_elems = rlx_ir::batch_region_slice_elems(&node.shape, n)
985 .expect("batch region static shape");
986 let mut win_ids: Vec<NodeId> = vec![node.id];
987 win_ids.extend(node.inputs.iter().copied());
988 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
989 let fits = arena_span_bytes(&arena, &win_ids) <= max_binding;
990 let mut scratch = arena.scratch_off as u64;
991 let (mut base, mut size, param_anchor) = arena_multi_op_window(
992 &dev.device,
993 &arena,
994 &graph,
995 ¶m_offsets,
996 &mut schedule,
997 &mut scratch,
998 &win_ids,
999 );
1000 if !fits && !param_anchor {
1001 base = arena_bind_window_covering_scratch_if_needed(
1002 &arena, base, size, scratch,
1003 );
1004 }
1005 let chain_enc = rlx_ir::encode_chain_steps(chain);
1006 let tail =
1007 rlx_ir::encode_prologue_tail(*prologue, &slice_shape, *prologue_input);
1008 let base_dst = arena_local_off_f32(&arena, node.id, base);
1009 let use_single = rlx_ir::fk_batch_use_single_launch(n, *prologue);
1010 if use_single {
1011 let mut batch_input_offs = [0u32; 64];
1012 for i in 0..n {
1013 batch_input_offs[i] = arena_off_in_bind_window(
1014 &graph,
1015 ¶m_offsets,
1016 &dev.device,
1017 &arena,
1018 &mut schedule,
1019 &mut scratch,
1020 node.inputs[i],
1021 &mut base,
1022 &mut size,
1023 );
1024 }
1025 let p = BatchElementwiseRegionParams {
1026 slice_len: slice_elems,
1027 num_batch: n as u32,
1028 num_steps: chain.len() as u32,
1029 base_dst_off: base_dst,
1030 slice_elems,
1031 batch_input_offs,
1032 chain: chain_enc,
1033 scalar_input_mask: *scalar_input_mask,
1034 input_modulus: *input_modulus,
1035 };
1036 schedule.push(Step::BatchElementwiseRegion { params: p });
1037 let ek = batch_elementwise_region_kernel(&dev.device);
1038 let u = dev.device.create_buffer(&wgpu::BufferDescriptor {
1039 label: Some("rlx-wgpu batch region params"),
1040 size: std::mem::size_of::<BatchElementwiseRegionParams>() as u64,
1041 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1042 mapped_at_creation: false,
1043 });
1044 let bg =
1045 bind_two_buf0_window(&dev.device, ek, &arena.buffer, base, size, &u);
1046 uniforms.push(u);
1047 bind_groups.push(bg);
1048 } else {
1049 let spatial = tail[0] == rlx_ir::REGION_PROLOGUE_RESIZE_NEAREST_2X_NCHW;
1050 let ek = if spatial {
1051 elementwise_region_spatial_kernel(&dev.device)
1052 } else {
1053 elementwise_region_kernel(&dev.device)
1054 };
1055 for i in 0..n {
1056 let mut input_offs = [0u32; 16];
1057 input_offs[0] = arena_off_in_bind_window(
1058 &graph,
1059 ¶m_offsets,
1060 &dev.device,
1061 &arena,
1062 &mut schedule,
1063 &mut scratch,
1064 node.inputs[i],
1065 &mut base,
1066 &mut size,
1067 );
1068 let p = ElementwiseRegionParams {
1069 len: slice_elems,
1070 num_inputs: 1,
1071 num_steps: chain.len() as u32,
1072 dst_off: rlx_ir::batch_region_slice_dst_off_f32(
1073 base_dst,
1074 slice_elems,
1075 i,
1076 ),
1077 input_offs,
1078 chain: chain_enc,
1079 scalar_input_mask: *scalar_input_mask,
1080 prologue: tail[0],
1081 out_n: tail[1],
1082 out_c: tail[2],
1083 out_h: tail[3],
1084 out_w: tail[4],
1085 prologue_input: tail[5],
1086 input_modulus: *input_modulus,
1087 };
1088 schedule.push(Step::ElementwiseRegion { params: p });
1089 let u = dev.device.create_buffer(&wgpu::BufferDescriptor {
1090 label: Some("rlx-wgpu batch region params"),
1091 size: std::mem::size_of::<ElementwiseRegionParams>() as u64,
1092 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1093 mapped_at_creation: false,
1094 });
1095 let bg = bind_two_buf0_window(
1096 &dev.device,
1097 ek,
1098 &arena.buffer,
1099 base,
1100 size,
1101 &u,
1102 );
1103 uniforms.push(u);
1104 bind_groups.push(bg);
1105 }
1106 }
1107 }
1108 Op::ElementwiseRegion {
1109 chain,
1110 num_inputs,
1111 scalar_input_mask,
1112 input_modulus,
1113 prologue,
1114 prologue_input,
1115 } => {
1116 let n = *num_inputs as usize;
1119 if n > 16 || chain.len() > 32 {
1120 panic!(
1121 "rlx-wgpu ElementwiseRegion: chain too large \
1122 (inputs={n}, steps={}). Caps: 16 / 32. \
1123 Use UnfuseElementwiseRegions to fall back.",
1124 chain.len()
1125 );
1126 }
1127 let mut win_ids: Vec<NodeId> = vec![node.id];
1128 win_ids.extend(node.inputs.iter().copied());
1129 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
1130 let fits = arena_span_bytes(&arena, &win_ids) <= max_binding;
1131 let mut scratch = arena.scratch_off as u64;
1132 let (mut base, mut size, param_anchor) = arena_multi_op_window(
1133 &dev.device,
1134 &arena,
1135 &graph,
1136 ¶m_offsets,
1137 &mut schedule,
1138 &mut scratch,
1139 &win_ids,
1140 );
1141 if !fits && !param_anchor {
1142 base = arena_bind_window_covering_scratch_if_needed(
1143 &arena, base, size, scratch,
1144 );
1145 }
1146 let mut input_offs = [0u32; 16];
1147 for (i, &id) in node.inputs.iter().enumerate() {
1148 input_offs[i] = arena_off_in_bind_window(
1149 &graph,
1150 ¶m_offsets,
1151 &dev.device,
1152 &arena,
1153 &mut schedule,
1154 &mut scratch,
1155 id,
1156 &mut base,
1157 &mut size,
1158 );
1159 }
1160 let chain_enc = rlx_ir::encode_chain_steps(chain);
1161 let tail =
1162 rlx_ir::encode_prologue_tail(*prologue, &node.shape, *prologue_input);
1163 let p = ElementwiseRegionParams {
1164 len: elems,
1165 num_inputs: *num_inputs,
1166 num_steps: chain.len() as u32,
1167 dst_off: arena_local_off_f32(&arena, node.id, base),
1168 input_offs,
1169 chain: chain_enc,
1170 scalar_input_mask: *scalar_input_mask,
1171 prologue: tail[0],
1172 out_n: tail[1],
1173 out_c: tail[2],
1174 out_h: tail[3],
1175 out_w: tail[4],
1176 prologue_input: tail[5],
1177 input_modulus: *input_modulus,
1178 };
1179 schedule.push(Step::ElementwiseRegion { params: p });
1180 let ek = if p.prologue == rlx_ir::REGION_PROLOGUE_RESIZE_NEAREST_2X_NCHW {
1181 elementwise_region_spatial_kernel(&dev.device)
1182 } else {
1183 elementwise_region_kernel(&dev.device)
1184 };
1185 let u = dev.device.create_buffer(&wgpu::BufferDescriptor {
1189 label: Some("rlx-wgpu region params"),
1190 size: std::mem::size_of::<ElementwiseRegionParams>() as u64,
1191 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1192 mapped_at_creation: false,
1193 });
1194 let bg = bind_two_buf0_window(&dev.device, ek, &arena.buffer, base, size, &u);
1195 uniforms.push(u);
1196 bind_groups.push(bg);
1197 }
1198
1199 Op::Reduce {
1200 op: rop,
1201 axes,
1202 keep_dim: _,
1203 } => {
1204 let in_id = node.inputs[0];
1212 let in_shape = graph.node(in_id).shape.dims();
1213 let mut sorted = axes.clone();
1214 sorted.sort_unstable();
1215 let contiguous = sorted.windows(2).all(|w| w[1] == w[0] + 1);
1216 if !contiguous {
1217 panic!(
1218 "rlx-wgpu Reduce: non-contiguous axes not yet wired \
1219 (got axes={axes:?}, rank={})",
1220 in_shape.len()
1221 );
1222 }
1223 let ax_first = sorted[0];
1224 let ax_last = *sorted.last().unwrap();
1225 let dims_u32: Vec<u32> =
1226 in_shape.iter().map(|d| d.unwrap_static() as u32).collect();
1227 let outer: u32 = dims_u32[..ax_first].iter().product();
1228 let reduce_dim: u32 = dims_u32[ax_first..=ax_last].iter().product();
1229 let inner: u32 = dims_u32[ax_last + 1..].iter().product();
1230 let red_ids = [node.id, in_id];
1231 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
1232 let red_fits = arena_span_bytes(&arena, &red_ids) <= max_binding;
1233 let mut scratch = arena.scratch_off as u64;
1234 let (mut base, mut size, param_anchor) = arena_multi_op_window(
1235 &dev.device,
1236 &arena,
1237 &graph,
1238 ¶m_offsets,
1239 &mut schedule,
1240 &mut scratch,
1241 &red_ids,
1242 );
1243 if !red_fits && !param_anchor {
1244 base = arena_bind_window_covering_scratch_if_needed(
1245 &arena, base, size, scratch,
1246 );
1247 }
1248 let in_off = arena_off_in_bind_window(
1249 &graph,
1250 ¶m_offsets,
1251 &dev.device,
1252 &arena,
1253 &mut schedule,
1254 &mut scratch,
1255 in_id,
1256 &mut base,
1257 &mut size,
1258 );
1259 let p = ReduceParams {
1260 outer,
1261 reduce_dim,
1262 inner,
1263 in_off,
1264 out_off: arena_local_off_f32(&arena, node.id, base),
1265 op: reduce_op_id(*rop),
1266 _p0: 0,
1267 _p1: 0,
1268 };
1269 schedule.push(Step::Reduce { params: p });
1270 let rk = reduce_kernel(&dev.device);
1271 let u = emit_uniform(std::mem::size_of::<ReduceParams>());
1272 let bg = bind_two_buf0_window(&dev.device, rk, &arena.buffer, base, size, &u);
1273 uniforms.push(u);
1274 bind_groups.push(bg);
1275 }
1276
1277 Op::Softmax { axis } => {
1278 let in_id = node.inputs[0];
1279 let in_shape = graph.node(in_id).shape.dims();
1280 let last = (in_shape.len() - 1) as i32;
1281 if *axis != -1 && *axis != last {
1282 panic!("rlx-wgpu Softmax: only last-axis wired (got axis={axis})");
1283 }
1284 let inner = in_shape[in_shape.len() - 1].unwrap_static() as u32;
1285 let total: u32 = in_shape.iter().map(|d| d.unwrap_static() as u32).product();
1286 let outer = total / inner.max(1);
1287 let sm_ids = [node.id, in_id];
1288 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
1289 let sm_fits = arena_span_bytes(&arena, &sm_ids) <= max_binding;
1290 let mut scratch = arena.scratch_off as u64;
1291 let (mut base, mut size, param_anchor) = arena_multi_op_window(
1292 &dev.device,
1293 &arena,
1294 &graph,
1295 ¶m_offsets,
1296 &mut schedule,
1297 &mut scratch,
1298 &sm_ids,
1299 );
1300 if !sm_fits && !param_anchor {
1301 base = arena_bind_window_covering_scratch_if_needed(
1302 &arena, base, size, scratch,
1303 );
1304 }
1305 let in_off = arena_off_in_bind_window(
1306 &graph,
1307 ¶m_offsets,
1308 &dev.device,
1309 &arena,
1310 &mut schedule,
1311 &mut scratch,
1312 in_id,
1313 &mut base,
1314 &mut size,
1315 );
1316 let p = SoftmaxParams {
1317 outer,
1318 inner,
1319 in_off,
1320 out_off: arena_local_off_f32(&arena, node.id, base),
1321 _p0: 0,
1322 _p1: 0,
1323 _p2: 0,
1324 _p3: 0,
1325 };
1326 schedule.push(Step::Softmax { params: p });
1327 let sk = softmax_kernel(&dev.device);
1328 let u = emit_uniform(std::mem::size_of::<SoftmaxParams>());
1329 let bg = bind_two_buf0_window(&dev.device, sk, &arena.buffer, base, size, &u);
1330 uniforms.push(u);
1331 bind_groups.push(bg);
1332 }
1333
1334 Op::SoftmaxCrossEntropy => {
1335 let logits_id = node.inputs[0];
1340 let targets_id = node.inputs[1];
1341 let logits_shape = graph.node(logits_id).shape.dims();
1342 let inner = logits_shape[logits_shape.len() - 1].unwrap_static() as u32;
1343 let total: u32 = logits_shape
1344 .iter()
1345 .map(|d| d.unwrap_static() as u32)
1346 .product();
1347 let outer = total / inner.max(1);
1348
1349 let sce_win = vec![node.id, logits_id, targets_id];
1350 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
1351 let sce_fits = arena_span_bytes(&arena, &sce_win) <= max_binding;
1352 let mut scratch = arena.scratch_off as u64;
1353 let (mut base, mut size, param_anchor) = arena_multi_op_window(
1354 &dev.device,
1355 &arena,
1356 &graph,
1357 ¶m_offsets,
1358 &mut schedule,
1359 &mut scratch,
1360 &sce_win,
1361 );
1362 if !sce_fits && !param_anchor {
1363 base = arena_bind_window_covering_scratch_if_needed(
1364 &arena, base, size, scratch,
1365 );
1366 }
1367 let logits_off = arena_off_in_bind_window(
1368 &graph,
1369 ¶m_offsets,
1370 &dev.device,
1371 &arena,
1372 &mut schedule,
1373 &mut scratch,
1374 logits_id,
1375 &mut base,
1376 &mut size,
1377 );
1378 let targets_off = arena_off_in_bind_window(
1379 &graph,
1380 ¶m_offsets,
1381 &dev.device,
1382 &arena,
1383 &mut schedule,
1384 &mut scratch,
1385 targets_id,
1386 &mut base,
1387 &mut size,
1388 );
1389 let p = SceParams {
1390 outer,
1391 inner,
1392 logits_off,
1393 targets_off,
1394 out_off: arena_local_off_f32(&arena, node.id, base),
1395 _p0: 0,
1396 _p1: 0,
1397 _p2: 0,
1398 };
1399 schedule.push(Step::SoftmaxCrossEntropy { params: p });
1400 let sk = softmax_cross_entropy_kernel(&dev.device);
1401 let u = emit_uniform(std::mem::size_of::<SceParams>());
1402 let bg = bind_two_buf0_window(&dev.device, sk, &arena.buffer, base, size, &u);
1403 uniforms.push(u);
1404 bind_groups.push(bg);
1405 }
1406
1407 Op::LayerNorm { axis: _, eps } | Op::RmsNorm { axis: _, eps } => {
1408 let in_id = node.inputs[0];
1409 let in_shape = graph.node(in_id).shape.dims();
1410 let inner = in_shape[in_shape.len() - 1].unwrap_static() as u32;
1411 let total: u32 = in_shape.iter().map(|d| d.unwrap_static() as u32).product();
1412 let outer = total / inner.max(1);
1413 let is_layer_norm = matches!(&node.op, Op::LayerNorm { .. });
1414
1415 if is_layer_norm
1422 && let Some(&(h_id, delta_id, gamma_id, beta_id, sum_id)) =
1423 ln_to_tee.get(&node.id)
1424 {
1425 let gamma_is_param =
1426 tensor_is_graph_param(&graph, ¶m_offsets, gamma_id);
1427 let gamma_bytes = arena.len_of(gamma_id) as u64;
1428 let frlt_win: Vec<NodeId> =
1429 if gamma_is_param && gamma_bytes > ARENA_STAGE_CAP {
1430 vec![gamma_id, node.id, h_id, delta_id, beta_id, sum_id]
1431 } else {
1432 vec![node.id, h_id, delta_id, gamma_id, beta_id, sum_id]
1433 };
1434 let mut scratch = arena.scratch_off as u64;
1435 let (mut base, mut size, param_anchor) = arena_multi_op_window(
1436 &dev.device,
1437 &arena,
1438 &graph,
1439 ¶m_offsets,
1440 &mut schedule,
1441 &mut scratch,
1442 &frlt_win,
1443 );
1444 if !param_anchor {
1445 base = arena_bind_window_covering_scratch_if_needed(
1446 &arena, base, size, scratch,
1447 );
1448 }
1449 let in_off = arena_off_in_bind_window(
1450 &graph,
1451 ¶m_offsets,
1452 &dev.device,
1453 &arena,
1454 &mut schedule,
1455 &mut scratch,
1456 h_id,
1457 &mut base,
1458 &mut size,
1459 );
1460 let residual_off = arena_off_in_bind_window(
1461 &graph,
1462 ¶m_offsets,
1463 &dev.device,
1464 &arena,
1465 &mut schedule,
1466 &mut scratch,
1467 delta_id,
1468 &mut base,
1469 &mut size,
1470 );
1471 let sum_off = arena_off_in_bind_window(
1472 &graph,
1473 ¶m_offsets,
1474 &dev.device,
1475 &arena,
1476 &mut schedule,
1477 &mut scratch,
1478 sum_id,
1479 &mut base,
1480 &mut size,
1481 );
1482 let gamma_off = arena_off_in_bind_window(
1483 &graph,
1484 ¶m_offsets,
1485 &dev.device,
1486 &arena,
1487 &mut schedule,
1488 &mut scratch,
1489 gamma_id,
1490 &mut base,
1491 &mut size,
1492 );
1493 let beta_off = arena_off_in_bind_window(
1494 &graph,
1495 ¶m_offsets,
1496 &dev.device,
1497 &arena,
1498 &mut schedule,
1499 &mut scratch,
1500 beta_id,
1501 &mut base,
1502 &mut size,
1503 );
1504 let p = FusedResidualLnTeeParams {
1505 outer,
1506 inner,
1507 in_off,
1508 residual_off,
1509 bias_off: 0, gamma_off,
1511 beta_off,
1512 sum_off,
1513 ln_out_off: arena_local_off_f32(&arena, node.id, base),
1514 eps_bits: eps.to_bits(),
1515 has_bias: 0,
1516 _p0: 0,
1517 };
1518 schedule.push(Step::FusedResidualLnTee { params: p });
1519 let frtk = fused_residual_ln_tee_kernel(&dev.device);
1520 let u = emit_uniform(std::mem::size_of::<FusedResidualLnTeeParams>());
1521 let bg =
1522 bind_two_buf0_window(&dev.device, frtk, &arena.buffer, base, size, &u);
1523 uniforms.push(u);
1524 bind_groups.push(bg);
1525 continue;
1526 }
1527
1528 let gamma_id = node.inputs[1];
1529 let beta_id = if is_layer_norm && node.inputs.len() >= 3 {
1532 node.inputs[2]
1533 } else {
1534 gamma_id
1537 };
1538 let gamma_is_param = tensor_is_graph_param(&graph, ¶m_offsets, gamma_id);
1539 let gamma_bytes = arena.len_of(gamma_id) as u64;
1540 let ln_win: Vec<NodeId> = if gamma_is_param && gamma_bytes > ARENA_STAGE_CAP {
1541 vec![gamma_id, node.id, in_id]
1542 } else {
1543 let mut v = vec![node.id, in_id];
1544 if gamma_is_param {
1545 v.push(gamma_id);
1546 }
1547 if is_layer_norm {
1548 v.push(beta_id);
1549 }
1550 v
1551 };
1552 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
1553 let ln_fits = arena_span_bytes(&arena, &ln_win) <= max_binding;
1554 let mut scratch = arena.scratch_off as u64;
1555 let (mut base, mut size, param_anchor) = arena_multi_op_window(
1556 &dev.device,
1557 &arena,
1558 &graph,
1559 ¶m_offsets,
1560 &mut schedule,
1561 &mut scratch,
1562 &ln_win,
1563 );
1564 if !ln_fits && !param_anchor {
1565 base = arena_bind_window_covering_scratch_if_needed(
1566 &arena, base, size, scratch,
1567 );
1568 }
1569 let in_off = arena_off_in_bind_window(
1570 &graph,
1571 ¶m_offsets,
1572 &dev.device,
1573 &arena,
1574 &mut schedule,
1575 &mut scratch,
1576 in_id,
1577 &mut base,
1578 &mut size,
1579 );
1580 let gamma_off = arena_off_in_bind_window(
1581 &graph,
1582 ¶m_offsets,
1583 &dev.device,
1584 &arena,
1585 &mut schedule,
1586 &mut scratch,
1587 gamma_id,
1588 &mut base,
1589 &mut size,
1590 );
1591 let beta_off = arena_off_in_bind_window(
1592 &graph,
1593 ¶m_offsets,
1594 &dev.device,
1595 &arena,
1596 &mut schedule,
1597 &mut scratch,
1598 beta_id,
1599 &mut base,
1600 &mut size,
1601 );
1602 let p = LayerNormParams {
1603 outer,
1604 inner,
1605 in_off,
1606 out_off: arena_local_off_f32(&arena, node.id, base),
1607 gamma_off,
1608 beta_off,
1609 eps_bits: eps.to_bits(),
1610 op: if is_layer_norm { 0 } else { 1 },
1611 };
1612 schedule.push(Step::LayerNorm { params: p });
1613 let lk = layernorm_kernel(&dev.device);
1614 let u = emit_uniform(std::mem::size_of::<LayerNormParams>());
1615 let bg = bind_two_buf0_window(&dev.device, lk, &arena.buffer, base, size, &u);
1616 uniforms.push(u);
1617 bind_groups.push(bg);
1618 }
1619
1620 Op::Reshape { .. } => {
1621 }
1623
1624 Op::Cast { .. } => {
1625 let in_id = node.inputs[0];
1632 let src = arena.offset(in_id);
1633 let dst = arena.offset(node.id);
1634 if src != dst {
1635 let bytes = arena.len_of(in_id).min(arena.len_of(node.id));
1636 schedule.push(Step::BufferCopy {
1637 src_byte_off: src as u64,
1638 dst_byte_off: dst as u64,
1639 bytes: bytes as u32,
1640 });
1641 }
1642 }
1643
1644 Op::Transpose { perm } => {
1645 let in_id = node.inputs[0];
1646 let in_shape = graph.node(in_id).shape.dims();
1647 let out_shape = node.shape.dims();
1648 let rank = perm.len();
1649 if rank != in_shape.len() || rank != out_shape.len() {
1650 panic!("rlx-wgpu Transpose: rank mismatch");
1651 }
1652 let in_dims: Vec<u32> =
1653 in_shape.iter().map(|d| d.unwrap_static() as u32).collect();
1654 let out_dims: Vec<u32> =
1655 out_shape.iter().map(|d| d.unwrap_static() as u32).collect();
1656 let mut in_strides = vec![1u32; rank];
1658 for i in (0..rank.saturating_sub(1)).rev() {
1659 in_strides[i] = in_strides[i + 1] * in_dims[i + 1];
1660 }
1661 let strides_for_out: Vec<u32> =
1664 (0..rank).map(|i| in_strides[perm[i]]).collect();
1665
1666 let mut meta_data: Vec<u32> = Vec::with_capacity(rank * 2);
1668 meta_data.extend_from_slice(&out_dims);
1669 meta_data.extend_from_slice(&strides_for_out);
1670 let meta_buf = dev.device.create_buffer(&wgpu::BufferDescriptor {
1671 label: Some("rlx-wgpu transpose meta"),
1672 size: (meta_data.len() * 4).max(4) as u64,
1673 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1674 mapped_at_creation: false,
1675 });
1676 dev.queue
1677 .write_buffer(&meta_buf, 0, bytemuck::cast_slice(&meta_data));
1678 let meta_idx = meta_buffers.len();
1679 meta_buffers.push(meta_buf);
1680
1681 let bucket_outermost = if perm[0] == 0 { 1u32 } else { 0u32 };
1685 let tr_ids = [node.id, in_id];
1686 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
1687 let in_is_param = tensor_is_graph_param(&graph, ¶m_offsets, in_id);
1688 let in_bytes = arena.len_of(in_id) as u64;
1689 let (mut base, mut size) = if in_is_param && in_bytes <= max_binding {
1690 arena_window_for_nodes(&dev.device, &arena, &[in_id])
1691 } else if arena_span_bytes(&arena, &tr_ids) <= max_binding {
1692 arena_window_for_nodes(&dev.device, &arena, &tr_ids)
1693 } else {
1694 arena_window_for_nodes(&dev.device, &arena, &[node.id])
1695 };
1696 let mut scratch = arena.scratch_off as u64;
1697 let in_off = arena_off_in_bind_window(
1698 &graph,
1699 ¶m_offsets,
1700 &dev.device,
1701 &arena,
1702 &mut schedule,
1703 &mut scratch,
1704 in_id,
1705 &mut base,
1706 &mut size,
1707 );
1708 let out_off = arena_off_in_bind_window(
1709 &graph,
1710 ¶m_offsets,
1711 &dev.device,
1712 &arena,
1713 &mut schedule,
1714 &mut scratch,
1715 node.id,
1716 &mut base,
1717 &mut size,
1718 );
1719 let p = TransposeParams {
1720 rank: rank as u32,
1721 out_total: elems,
1722 in_off,
1723 out_off,
1724 bucket_outermost,
1725 out_dim_0: out_dims[0],
1726 _p2: 0,
1727 _p3: 0,
1728 };
1729 schedule.push(Step::Transpose {
1730 params: p,
1731 meta_idx,
1732 });
1733 let tk = transpose_kernel(&dev.device);
1734 let u = emit_uniform(std::mem::size_of::<TransposeParams>());
1735 let bg = dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
1736 label: Some("rlx-wgpu transpose bg"),
1737 layout: &tk.bgl,
1738 entries: &[
1739 wgpu::BindGroupEntry {
1740 binding: 0,
1741 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1742 buffer: &arena.buffer,
1743 offset: base,
1744 size: aligned_bind_size(size, base, arena.buffer.size()),
1745 }),
1746 },
1747 wgpu::BindGroupEntry {
1748 binding: 1,
1749 resource: u.as_entire_binding(),
1750 },
1751 wgpu::BindGroupEntry {
1752 binding: 2,
1753 resource: meta_buffers[meta_idx].as_entire_binding(),
1754 },
1755 ],
1756 });
1757 uniforms.push(u);
1758 bind_groups.push(bg);
1759 }
1760
1761 Op::Narrow { axis, start, len } => {
1762 if qkv_skip_narrows.contains(&node.id)
1767 || packed_bshd_skip_narrows.contains(&node.id)
1768 {
1769 continue;
1770 }
1771 let in_id = node.inputs[0];
1772 let in_shape = graph.node(in_id).shape.dims();
1773 let outer: u32 = in_shape[..*axis]
1774 .iter()
1775 .map(|d| d.unwrap_static() as u32)
1776 .product::<u32>()
1777 .max(1);
1778 let inner: u32 = in_shape[*axis + 1..]
1779 .iter()
1780 .map(|d| d.unwrap_static() as u32)
1781 .product::<u32>()
1782 .max(1);
1783 let axis_in = in_shape[*axis].unwrap_static() as u32;
1784 let win_ids = [node.id, in_id];
1785 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
1786 let fits = arena_span_bytes(&arena, &win_ids) <= max_binding;
1787 let mut scratch = arena.scratch_off as u64;
1788 let (mut base, mut size, param_anchor) = arena_multi_op_window(
1789 &dev.device,
1790 &arena,
1791 &graph,
1792 ¶m_offsets,
1793 &mut schedule,
1794 &mut scratch,
1795 &win_ids,
1796 );
1797 if !fits && !param_anchor {
1798 base = arena_bind_window_covering_scratch_if_needed(
1799 &arena, base, size, scratch,
1800 );
1801 }
1802 let in_off = arena_off_in_bind_window(
1803 &graph,
1804 ¶m_offsets,
1805 &dev.device,
1806 &arena,
1807 &mut schedule,
1808 &mut scratch,
1809 in_id,
1810 &mut base,
1811 &mut size,
1812 );
1813 let out_off = arena_off_in_bind_window(
1814 &graph,
1815 ¶m_offsets,
1816 &dev.device,
1817 &arena,
1818 &mut schedule,
1819 &mut scratch,
1820 node.id,
1821 &mut base,
1822 &mut size,
1823 );
1824 let p = NarrowConcatParams {
1825 total: elems,
1826 outer,
1827 inner,
1828 axis_in_size: axis_in,
1829 axis_out_size: *len as u32,
1830 start: *start as u32,
1831 in_off,
1832 out_off,
1833 };
1834 schedule.push(Step::Narrow { params: p });
1835 let nk = narrow_kernel(&dev.device);
1836 let u = emit_uniform(std::mem::size_of::<NarrowConcatParams>());
1837 let bg = bind_two_buf0_window(&dev.device, nk, &arena.buffer, base, size, &u);
1838 uniforms.push(u);
1839 bind_groups.push(bg);
1840 }
1841
1842 Op::Concat { axis } => {
1843 let out_shape = node.shape.dims();
1844 let outer: u32 = out_shape[..*axis]
1845 .iter()
1846 .map(|d| d.unwrap_static() as u32)
1847 .product::<u32>()
1848 .max(1);
1849 let inner: u32 = out_shape[*axis + 1..]
1850 .iter()
1851 .map(|d| d.unwrap_static() as u32)
1852 .product::<u32>()
1853 .max(1);
1854 let axis_out = out_shape[*axis].unwrap_static() as u32;
1855
1856 let all_ids: Vec<NodeId> = std::iter::once(node.id)
1857 .chain(node.inputs.iter().copied())
1858 .collect();
1859 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
1860 let fits_all = arena_span_bytes(&arena, &all_ids) <= max_binding;
1861 let mut scratch = arena.scratch_off as u64;
1862 let (mut base, mut size, param_anchor) = arena_multi_op_window(
1863 &dev.device,
1864 &arena,
1865 &graph,
1866 ¶m_offsets,
1867 &mut schedule,
1868 &mut scratch,
1869 &all_ids,
1870 );
1871 arena_expand_bind_window(&arena, &all_ids, &mut base, &mut size, max_binding);
1872 if !fits_all && !param_anchor {
1873 base = arena_bind_window_covering_scratch_if_needed(
1874 &arena, base, size, scratch,
1875 );
1876 }
1877 let mut start_pos: u32 = 0;
1878 for &in_id in &node.inputs {
1879 let in_shape = graph.node(in_id).shape.dims();
1880 let axis_in = in_shape[*axis].unwrap_static() as u32;
1881 let in_total: u32 =
1882 in_shape.iter().map(|d| d.unwrap_static() as u32).product();
1883 let _win_ids = [node.id, in_id];
1884 let in_off = arena_off_in_bind_window(
1885 &graph,
1886 ¶m_offsets,
1887 &dev.device,
1888 &arena,
1889 &mut schedule,
1890 &mut scratch,
1891 in_id,
1892 &mut base,
1893 &mut size,
1894 );
1895 let out_off = arena_local_off_f32(&arena, node.id, base);
1901 let p = NarrowConcatParams {
1902 total: in_total,
1903 outer,
1904 inner,
1905 axis_in_size: axis_in,
1906 axis_out_size: axis_out,
1907 start: start_pos,
1908 in_off,
1909 out_off,
1910 };
1911 schedule.push(Step::Concat { params: p });
1912 let cck = concat_kernel(&dev.device);
1913 let u = emit_uniform(std::mem::size_of::<NarrowConcatParams>());
1914 let bg =
1915 bind_two_buf0_window(&dev.device, cck, &arena.buffer, base, size, &u);
1916 uniforms.push(u);
1917 bind_groups.push(bg);
1918 start_pos += axis_in;
1919 }
1920 }
1921
1922 Op::Attention {
1923 num_heads,
1924 head_dim,
1925 mask_kind,
1926 score_scale,
1927 attn_logit_softcap: _,
1928 } => {
1929 let q_id = node.inputs[0];
1932 let k_id = node.inputs[1];
1933 let v_id = node.inputs[2];
1934 let q_shape = graph.node(q_id).shape.dims();
1935 let k_shape = graph.node(k_id).shape.dims();
1936 let h = *num_heads as u32;
1942 let hd = *head_dim as u32;
1943 let q_ir = graph.node(q_id).shape.clone();
1944 let k_ir = graph.node(k_id).shape.clone();
1945 let geom = rlx_ir::attention_geom(&q_ir, &k_ir, *num_heads, *head_dim);
1946 let bhsd = geom.bhsd;
1947 let (batch, heads, seq_q, seq_k) = match q_shape.len() {
1948 4 => (
1949 geom.batch as u32,
1950 geom.heads as u32,
1951 geom.seq_q as u32,
1952 geom.seq_k as u32,
1953 ),
1954 3 => {
1955 let last = q_shape[2].unwrap_static() as u32;
1962 if last == h * hd {
1963 (
1965 q_shape[0].unwrap_static() as u32,
1966 h,
1967 q_shape[1].unwrap_static() as u32,
1968 k_shape[1].unwrap_static() as u32,
1969 )
1970 } else {
1971 let leading = q_shape[0].unwrap_static() as u32;
1973 if !leading.is_multiple_of(h) {
1974 panic!(
1975 "rlx-wgpu Attention: rank-3 leading dim {leading} \
1976 not divisible by num_heads {h} (and last dim \
1977 {last} ≠ H·D = {})",
1978 h * hd
1979 );
1980 }
1981 (
1982 leading / h,
1983 h,
1984 q_shape[1].unwrap_static() as u32,
1985 k_shape[1].unwrap_static() as u32,
1986 )
1987 }
1988 }
1989 other => panic!(
1990 "rlx-wgpu Attention: only rank-3 / rank-4 Q,K,V \
1991 inputs supported (got rank {other})"
1992 ),
1993 };
1994 let scale = score_scale.unwrap_or(1.0_f32 / (hd as f32).sqrt());
1995
1996 let (mask_kind_id, mask_buf, window) = match mask_kind {
1997 MaskKind::None => (0u32, None, 0u32),
1998 MaskKind::Causal => (1u32, None, 0u32),
1999 MaskKind::Custom => (2u32, None, 0u32),
2005 MaskKind::Bias => (4u32, None, 0u32),
2006 MaskKind::SlidingWindow(w) => (3u32, None, *w as u32),
2007 };
2008
2009 struct MStrides {
2016 b: u32,
2017 h: u32,
2018 q: u32,
2019 k: u32,
2020 }
2021 let mask_strides = if mask_kind_id == 2u32 || mask_kind_id == 4u32 {
2022 let m_dims = graph.node(node.inputs[3]).shape.dims();
2023 let dim = |i: usize| m_dims[i].unwrap_static() as u32;
2024 match m_dims.len() {
2025 2 => MStrides {
2026 b: dim(1),
2027 h: 0,
2028 q: 0,
2029 k: 1,
2030 },
2031 3 => MStrides {
2032 b: dim(1) * dim(2),
2033 h: 0,
2034 q: dim(2),
2035 k: 1,
2036 },
2037 4 => MStrides {
2038 b: dim(1) * dim(2) * dim(3),
2039 h: dim(2) * dim(3),
2040 q: dim(3),
2041 k: 1,
2042 },
2043 _ => MStrides {
2044 b: heads * seq_q * seq_k,
2045 h: seq_q * seq_k,
2046 q: seq_k,
2047 k: 1,
2048 },
2049 }
2050 } else {
2051 MStrides {
2052 b: heads * seq_q * seq_k,
2053 h: seq_q * seq_k,
2054 q: seq_k,
2055 k: 1,
2056 }
2057 };
2058
2059 let stride = |shape: &[rlx_ir::shape::Dim], seq_extent: u32| {
2060 rlx_ir::strides_for_shape(shape, heads, hd, seq_extent, bhsd)
2061 };
2062 let packed_parent = packed_bshd_attn.get(&node.id).copied();
2063 let nkv: u32 = if packed_parent.is_some() {
2067 heads
2068 } else {
2069 let k_numel: u32 =
2070 k_shape.iter().map(|d| d.unwrap_static() as u32).product();
2071 (k_numel / (batch.max(1) * seq_k.max(1) * hd.max(1))).max(1)
2072 };
2073 let (q_b, q_h, q_s, k_b, k_h, k_s, v_b, v_h, v_s) =
2074 if let Some((_parent, head_width)) = packed_parent {
2075 let (batch_stride, head_stride, pack_seq) =
2076 rlx_ir::packed_bshd_qkv_strides(head_width as usize, hd, seq_q);
2077 (
2078 batch_stride,
2079 head_stride,
2080 pack_seq,
2081 batch_stride,
2082 head_stride,
2083 pack_seq,
2084 batch_stride,
2085 head_stride,
2086 pack_seq,
2087 )
2088 } else {
2089 let (qb, qh, qs) = stride(q_shape, seq_q);
2090 let (kb, kh, ks) =
2093 rlx_ir::strides_for_shape(k_shape, nkv, hd, seq_k, bhsd);
2094 let v_shape = graph.node(v_id).shape.dims();
2095 let (vb, vh, vs) =
2096 rlx_ir::strides_for_shape(v_shape, nkv, hd, seq_k, bhsd);
2097 (qb, qh, qs, kb, kh, ks, vb, vh, vs)
2098 };
2099 let out_shape = node.shape.dims();
2100 let (o_b, o_h, o_s) = stride(out_shape, seq_q);
2101 let mut attn_ids = if let Some((parent, _)) = packed_parent {
2102 vec![node.id, parent]
2103 } else {
2104 vec![node.id, q_id, k_id, v_id]
2105 };
2106 if mask_kind_id == 2 {
2107 attn_ids.push(node.inputs[3]);
2108 }
2109 let mut scratch = arena.scratch_off as u64;
2110 let (mut base, mut size, param_anchor) = arena_multi_op_window(
2111 &dev.device,
2112 &arena,
2113 &graph,
2114 ¶m_offsets,
2115 &mut schedule,
2116 &mut scratch,
2117 &attn_ids,
2118 );
2119 if !param_anchor {
2120 base = arena_bind_window_covering_scratch_if_needed(
2121 &arena, base, size, scratch,
2122 );
2123 }
2124 let (q_off, k_off, v_off) = if let Some((parent, head_width)) = packed_parent {
2125 let parent_off = arena_off_in_bind_window(
2126 &graph,
2127 ¶m_offsets,
2128 &dev.device,
2129 &arena,
2130 &mut schedule,
2131 &mut scratch,
2132 parent,
2133 &mut base,
2134 &mut size,
2135 );
2136 (
2137 parent_off,
2138 parent_off.saturating_add(head_width),
2139 parent_off.saturating_add(head_width * 2),
2140 )
2141 } else {
2142 let q_off = arena_off_in_bind_window(
2143 &graph,
2144 ¶m_offsets,
2145 &dev.device,
2146 &arena,
2147 &mut schedule,
2148 &mut scratch,
2149 q_id,
2150 &mut base,
2151 &mut size,
2152 );
2153 let k_off = arena_off_in_bind_window(
2154 &graph,
2155 ¶m_offsets,
2156 &dev.device,
2157 &arena,
2158 &mut schedule,
2159 &mut scratch,
2160 k_id,
2161 &mut base,
2162 &mut size,
2163 );
2164 let v_off = arena_off_in_bind_window(
2165 &graph,
2166 ¶m_offsets,
2167 &dev.device,
2168 &arena,
2169 &mut schedule,
2170 &mut scratch,
2171 v_id,
2172 &mut base,
2173 &mut size,
2174 );
2175 (q_off, k_off, v_off)
2176 };
2177 let out_byte = arena.offset(node.id) as u64;
2178 let out_len = arena.len_of(node.id) as u64;
2179 let out_aliases_qkv = arena_tensors_overlap(&arena, node.id, q_id)
2180 || arena_tensors_overlap(&arena, node.id, k_id)
2181 || arena_tensors_overlap(&arena, node.id, v_id)
2182 || packed_parent.is_some_and(|(parent, _)| {
2183 arena_tensors_overlap(&arena, node.id, parent)
2184 });
2185 let mut kernel_out_off = arena_off_in_bind_window(
2186 &graph,
2187 ¶m_offsets,
2188 &dev.device,
2189 &arena,
2190 &mut schedule,
2191 &mut scratch,
2192 node.id,
2193 &mut base,
2194 &mut size,
2195 );
2196 let mut attn_scratch_copy: Option<(u64, u32)> = None;
2197 if out_aliases_qkv && rlx_ir::env::flag("RLX_WGPU_DEBUG_ATTN_ALIAS") {
2198 eprintln!(
2199 "rlx-wgpu Attention alias: out={:?}@{}+{} q={:?}@{} k={:?}@{} v={:?}@{}",
2200 node.id,
2201 out_byte,
2202 out_len,
2203 q_id,
2204 arena.offset(q_id),
2205 k_id,
2206 arena.offset(k_id),
2207 v_id,
2208 arena.offset(v_id),
2209 );
2210 }
2211 if out_aliases_qkv {
2212 let tmp_byte = scratch;
2213 let tmp_aligned = out_len.div_ceil(256) * 256;
2214 scratch = scratch.saturating_add(tmp_aligned);
2215 if param_anchor {
2216 arena_ensure_scratch_in_window(&mut scratch, base, size);
2217 } else {
2218 base = arena_bind_window_covering_scratch_if_needed(
2219 &arena, base, size, scratch,
2220 );
2221 }
2222 kernel_out_off = ((tmp_byte.saturating_sub(base)) / 4) as u32;
2223 attn_scratch_copy = Some((tmp_byte, out_len as u32));
2224 }
2225 let mask_off = if mask_kind_id == 2 {
2226 arena_off_in_bind_window(
2227 &graph,
2228 ¶m_offsets,
2229 &dev.device,
2230 &arena,
2231 &mut schedule,
2232 &mut scratch,
2233 node.inputs[3],
2234 &mut base,
2235 &mut size,
2236 )
2237 } else {
2238 0
2239 };
2240 let p = AttentionParams {
2241 batch,
2242 heads,
2243 seq_q,
2244 seq_k,
2245 head_dim: hd,
2246 q_off,
2247 k_off,
2248 v_off,
2249 out_off: kernel_out_off,
2250 mask_off,
2251 mask_kind: mask_kind_id,
2252 scale_bits: scale.to_bits(),
2253 window,
2254 seq_q_stride: mask_strides.q,
2263 seq_k_stride: mask_strides.k,
2264 mask_batch_stride: mask_strides.b,
2265 mask_head_stride: mask_strides.h,
2266 kv_heads: nkv,
2267 _pad_mask_1: 0,
2268 _pad_mask_2: 0,
2269 q_batch_stride: q_b,
2270 q_head_stride: q_h,
2271 q_seq_stride: q_s,
2272 _pad_q: 0,
2273 k_batch_stride: k_b,
2274 k_head_stride: k_h,
2275 k_seq_stride: k_s,
2276 _pad_k: 0,
2277 v_batch_stride: v_b,
2278 v_head_stride: v_h,
2279 v_seq_stride: v_s,
2280 _pad_v: 0,
2281 o_batch_stride: o_b,
2282 o_head_stride: o_h,
2283 o_seq_stride: o_s,
2284 _pad_o: 0,
2285 };
2286 let _ = num_heads;
2287 schedule.push(Step::Attention {
2288 params: p,
2289 mask_buf,
2290 });
2291 if let Some((tmp_byte, bytes)) = attn_scratch_copy {
2292 schedule.push(Step::BufferCopy {
2293 src_byte_off: tmp_byte,
2294 dst_byte_off: out_byte,
2295 bytes,
2296 });
2297 }
2298 let ak = attention_kernel(&dev.device);
2299 let u = emit_uniform(std::mem::size_of::<AttentionParams>());
2300 let bg = bind_two_buf0_window(&dev.device, ak, &arena.buffer, base, size, &u);
2301 uniforms.push(u);
2302 bind_groups.push(bg);
2303 }
2304
2305 Op::AttentionBackward {
2306 num_heads,
2307 head_dim,
2308 mask_kind,
2309 wrt,
2310 } => {
2311 use rlx_ir::op::AttentionBwdWrt;
2312 let q_id = node.inputs[0];
2313 let k_id = node.inputs[1];
2314 let v_id = node.inputs[2];
2315 let dy_id = node.inputs[3];
2316 let q_shape = graph.node(q_id).shape.dims();
2317 let k_shape = graph.node(k_id).shape.dims();
2318 let hd = *head_dim as u32;
2319 let q_ir = graph.node(q_id).shape.clone();
2320 let k_ir = graph.node(k_id).shape.clone();
2321 let geom = rlx_ir::attention_geom(&q_ir, &k_ir, *num_heads, *head_dim);
2322 let bhsd = geom.bhsd;
2323 let (batch, heads, seq_q, seq_k) = match q_shape.len() {
2324 4 => (
2325 geom.batch as u32,
2326 geom.heads as u32,
2327 geom.seq_q as u32,
2328 geom.seq_k as u32,
2329 ),
2330 3 => {
2331 let h = q_shape[2].unwrap_static() as u32 / hd;
2332 (
2333 q_shape[0].unwrap_static() as u32 / h,
2334 h,
2335 q_shape[1].unwrap_static() as u32,
2336 k_shape[1].unwrap_static() as u32,
2337 )
2338 }
2339 other => panic!(
2340 "rlx-wgpu AttentionBackward: only rank-3/4 Q,K,V (got rank {other})"
2341 ),
2342 };
2343 let scale = 1.0_f32 / (hd as f32).sqrt();
2344 let (mask_kind_id, mask_off, mask_buf, window) = match mask_kind {
2345 MaskKind::None => (0u32, 0u32, None, 0u32),
2346 MaskKind::Causal => (1u32, 0u32, None, 0u32),
2347 MaskKind::Custom => {
2348 (2u32, (arena.offset(node.inputs[4]) / 4) as u32, None, 0u32)
2349 }
2350 MaskKind::Bias => {
2351 (4u32, (arena.offset(node.inputs[4]) / 4) as u32, None, 0u32)
2352 }
2353 MaskKind::SlidingWindow(w) => (3u32, 0u32, None, *w as u32),
2354 };
2355 struct MStrides {
2356 b: u32,
2357 h: u32,
2358 q: u32,
2359 k: u32,
2360 }
2361 let mask_strides = if mask_kind_id == 2 || mask_kind_id == 4 {
2362 let m_dims = graph.node(node.inputs[4]).shape.dims();
2363 let dim = |i: usize| m_dims[i].unwrap_static() as u32;
2364 match m_dims.len() {
2365 2 => MStrides {
2366 b: dim(1),
2367 h: 0,
2368 q: 0,
2369 k: 1,
2370 },
2371 3 => MStrides {
2372 b: dim(1) * dim(2),
2373 h: 0,
2374 q: dim(2),
2375 k: 1,
2376 },
2377 4 => MStrides {
2378 b: dim(1) * dim(2) * dim(3),
2379 h: dim(2) * dim(3),
2380 q: dim(3),
2381 k: 1,
2382 },
2383 _ => MStrides {
2384 b: heads * seq_q * seq_k,
2385 h: seq_q * seq_k,
2386 q: seq_k,
2387 k: 1,
2388 },
2389 }
2390 } else {
2391 MStrides {
2392 b: heads * seq_q * seq_k,
2393 h: seq_q * seq_k,
2394 q: seq_k,
2395 k: 1,
2396 }
2397 };
2398 let stride = |shape: &[rlx_ir::shape::Dim], seq_extent: u32| {
2399 rlx_ir::strides_for_shape(shape, heads, hd, seq_extent, bhsd)
2400 };
2401 let (q_b, q_h, q_s) = stride(q_shape, seq_q);
2402 let (k_b, k_h, k_s) = stride(k_shape, seq_k);
2403 let v_shape = graph.node(v_id).shape.dims();
2404 let (v_b, v_h, v_s) = stride(v_shape, seq_k);
2405 let out_shape = node.shape.dims();
2406 let out_seq = match wrt {
2407 AttentionBwdWrt::Query => seq_q,
2408 AttentionBwdWrt::Key | AttentionBwdWrt::Value => seq_k,
2409 };
2410 let (o_b, o_h, o_s) = stride(out_shape, out_seq);
2411 let wrt_id = match wrt {
2412 AttentionBwdWrt::Query => 0u32,
2413 AttentionBwdWrt::Key => 1u32,
2414 AttentionBwdWrt::Value => 2u32,
2415 };
2416 let p = AttentionBwdParams {
2417 batch,
2418 heads,
2419 seq_q,
2420 seq_k,
2421 head_dim: hd,
2422 q_off: (arena.offset(q_id) / 4) as u32,
2423 k_off: (arena.offset(k_id) / 4) as u32,
2424 v_off: (arena.offset(v_id) / 4) as u32,
2425 dy_off: (arena.offset(dy_id) / 4) as u32,
2426 out_off: (arena.offset(node.id) / 4) as u32,
2427 mask_off,
2428 mask_kind: mask_kind_id,
2429 scale_bits: scale.to_bits(),
2430 window,
2431 wrt: wrt_id,
2432 seq_q_stride: mask_strides.q,
2433 seq_k_stride: mask_strides.k,
2434 mask_batch_stride: mask_strides.b,
2435 mask_head_stride: mask_strides.h,
2436 _pad_mask_0: 0,
2437 _pad_mask_1: 0,
2438 _pad_mask_2: 0,
2439 q_batch_stride: q_b,
2440 q_head_stride: q_h,
2441 q_seq_stride: q_s,
2442 _pad_q: 0,
2443 k_batch_stride: k_b,
2444 k_head_stride: k_h,
2445 k_seq_stride: k_s,
2446 _pad_k: 0,
2447 v_batch_stride: v_b,
2448 v_head_stride: v_h,
2449 v_seq_stride: v_s,
2450 _pad_v: 0,
2451 o_batch_stride: o_b,
2452 o_head_stride: o_h,
2453 o_seq_stride: o_s,
2454 _pad_o: 0,
2455 };
2456 schedule.push(Step::AttentionBackward {
2457 params: p,
2458 mask_buf,
2459 });
2460 let ak = attention_bwd_kernel(&dev.device);
2461 let u = emit_uniform(std::mem::size_of::<AttentionBwdParams>());
2462 let bg = bind_op_output_window(&dev.device, ak, &arena, node.id, &u);
2463 uniforms.push(u);
2464 bind_groups.push(bg);
2465 }
2466
2467 Op::Rope {
2468 head_dim,
2469 n_rot,
2470 style,
2471 } => {
2472 let x_id = node.inputs[0];
2473 let cos_id = node.inputs[1];
2474 let sin_id = node.inputs[2];
2475 let x_shape = graph.node(x_id).shape.dims();
2476 let last = x_shape.last().map(|d| d.unwrap_static()).unwrap_or(0);
2477 if !last.is_multiple_of(*head_dim) {
2478 panic!(
2479 "rlx-wgpu Rope: last_dim ({last}) must be a multiple \
2480 of head_dim ({head_dim})"
2481 );
2482 }
2483 if head_dim % 2 != 0 {
2484 panic!("rlx-wgpu Rope: head_dim must be even");
2485 }
2486 let total: u32 = x_shape.iter().map(|d| d.unwrap_static() as u32).product();
2487 let seq = x_shape[x_shape.len() - 2].unwrap_static() as u32;
2488 let batch = total / (seq * last as u32).max(1);
2493 let cos_is_param = tensor_is_graph_param(&graph, ¶m_offsets, cos_id);
2494 let cos_bytes = arena.len_of(cos_id) as u64;
2495 let rope_win: Vec<NodeId> = if cos_is_param && cos_bytes > ARENA_STAGE_CAP {
2496 vec![cos_id, sin_id, node.id, x_id]
2497 } else {
2498 vec![node.id, x_id, cos_id, sin_id]
2499 };
2500 let mut scratch = arena.scratch_off as u64;
2501 let (mut base, mut size, param_anchor) = arena_multi_op_window(
2502 &dev.device,
2503 &arena,
2504 &graph,
2505 ¶m_offsets,
2506 &mut schedule,
2507 &mut scratch,
2508 &rope_win,
2509 );
2510 if !param_anchor {
2511 base = arena_bind_window_covering_scratch_if_needed(
2512 &arena, base, size, scratch,
2513 );
2514 }
2515 let in_off = arena_off_in_bind_window(
2516 &graph,
2517 ¶m_offsets,
2518 &dev.device,
2519 &arena,
2520 &mut schedule,
2521 &mut scratch,
2522 x_id,
2523 &mut base,
2524 &mut size,
2525 );
2526 let cos_off = arena_off_in_bind_window(
2527 &graph,
2528 ¶m_offsets,
2529 &dev.device,
2530 &arena,
2531 &mut schedule,
2532 &mut scratch,
2533 cos_id,
2534 &mut base,
2535 &mut size,
2536 );
2537 let sin_off = arena_off_in_bind_window(
2538 &graph,
2539 ¶m_offsets,
2540 &dev.device,
2541 &arena,
2542 &mut schedule,
2543 &mut scratch,
2544 sin_id,
2545 &mut base,
2546 &mut size,
2547 );
2548 let p = RopeParams {
2549 n_total: total,
2550 seq,
2551 head_dim: *head_dim as u32,
2552 half: (*head_dim / 2) as u32,
2553 in_off,
2554 cos_off,
2555 sin_off,
2556 out_off: arena_local_off_f32(&arena, node.id, base),
2557 last_dim: last as u32,
2558 batch,
2559 seq_stride: seq,
2560 style: match style {
2561 rlx_ir::op::RopeStyle::NeoX => 0,
2562 rlx_ir::op::RopeStyle::GptJ => 1,
2563 },
2564 rot_half: (*n_rot / 2) as u32,
2567 };
2568 schedule.push(Step::Rope { params: p });
2569 let rk = rope_kernel(&dev.device);
2570 let u = emit_uniform(std::mem::size_of::<RopeParams>());
2571 let bg = bind_two_buf0_window(&dev.device, rk, &arena.buffer, base, size, &u);
2572 uniforms.push(u);
2573 bind_groups.push(bg);
2574 }
2575
2576 Op::Expand { target_shape } => {
2577 let in_id = node.inputs[0];
2578 let in_shape = graph.node(in_id).shape.dims();
2579 let in_rank = in_shape.len();
2580 let rank = target_shape.len();
2581 if in_rank > rank {
2582 panic!(
2583 "rlx-wgpu Expand: rank mismatch \
2584 (in_rank={in_rank}, target_rank={rank})"
2585 );
2586 }
2587 let pad = rank.saturating_sub(in_rank);
2590 let out_dims: Vec<u32> = target_shape.iter().map(|&d| d as u32).collect();
2591 let in_dims: Vec<u32> = (0..rank)
2592 .map(|i| {
2593 if i < pad {
2594 1
2595 } else {
2596 in_shape[i - pad].unwrap_static() as u32
2597 }
2598 })
2599 .collect();
2600 let mut in_strides_row = vec![1u32; rank];
2604 for i in (0..rank.saturating_sub(1)).rev() {
2605 in_strides_row[i] = in_strides_row[i + 1] * in_dims[i + 1];
2606 }
2607 let strides_for_out: Vec<u32> = (0..rank)
2608 .map(|i| {
2609 if in_dims[i] == 1 && out_dims[i] != 1 {
2610 0
2611 } else {
2612 in_strides_row[i]
2613 }
2614 })
2615 .collect();
2616
2617 let mut meta_data: Vec<u32> = Vec::with_capacity(rank * 2);
2618 meta_data.extend_from_slice(&out_dims);
2619 meta_data.extend_from_slice(&strides_for_out);
2620 let meta_buf = dev.device.create_buffer(&wgpu::BufferDescriptor {
2621 label: Some("rlx-wgpu expand meta"),
2622 size: (meta_data.len() * 4).max(4) as u64,
2623 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2624 mapped_at_creation: false,
2625 });
2626 dev.queue
2627 .write_buffer(&meta_buf, 0, bytemuck::cast_slice(&meta_data));
2628 let meta_idx = meta_buffers.len();
2629 meta_buffers.push(meta_buf);
2630
2631 let bucket_outermost = if in_dims[0] == out_dims[0] {
2637 1u32
2638 } else {
2639 0u32
2640 };
2641 let exp_ids = [node.id, in_id];
2642 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
2643 let exp_fits = arena_span_bytes(&arena, &exp_ids) <= max_binding;
2644 let mut scratch = arena.scratch_off as u64;
2645 let (mut base, mut size, param_anchor) = arena_multi_op_window(
2646 &dev.device,
2647 &arena,
2648 &graph,
2649 ¶m_offsets,
2650 &mut schedule,
2651 &mut scratch,
2652 &exp_ids,
2653 );
2654 if !exp_fits && !param_anchor {
2655 base = arena_bind_window_covering_scratch_if_needed(
2656 &arena, base, size, scratch,
2657 );
2658 }
2659 let in_off = arena_off_in_bind_window(
2660 &graph,
2661 ¶m_offsets,
2662 &dev.device,
2663 &arena,
2664 &mut schedule,
2665 &mut scratch,
2666 in_id,
2667 &mut base,
2668 &mut size,
2669 );
2670 let out_off = arena_off_in_bind_window(
2671 &graph,
2672 ¶m_offsets,
2673 &dev.device,
2674 &arena,
2675 &mut schedule,
2676 &mut scratch,
2677 node.id,
2678 &mut base,
2679 &mut size,
2680 );
2681 let p = ExpandParams {
2682 rank: rank as u32,
2683 out_total: elems,
2684 in_off,
2685 out_off,
2686 bucket_outermost,
2687 out_dim_0: out_dims[0],
2688 _p2: 0,
2689 _p3: 0,
2690 };
2691 schedule.push(Step::Expand {
2692 params: p,
2693 meta_idx,
2694 });
2695 let ek = expand_kernel(&dev.device);
2696 let u = emit_uniform(std::mem::size_of::<ExpandParams>());
2697 let bg = dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
2698 label: Some("rlx-wgpu expand bg"),
2699 layout: &ek.bgl,
2700 entries: &[
2701 wgpu::BindGroupEntry {
2702 binding: 0,
2703 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2704 buffer: &arena.buffer,
2705 offset: base,
2706 size: aligned_bind_size(size, base, arena.buffer.size()),
2707 }),
2708 },
2709 wgpu::BindGroupEntry {
2710 binding: 1,
2711 resource: u.as_entire_binding(),
2712 },
2713 wgpu::BindGroupEntry {
2714 binding: 2,
2715 resource: meta_buffers[meta_idx].as_entire_binding(),
2716 },
2717 ],
2718 });
2719 uniforms.push(u);
2720 bind_groups.push(bg);
2721 }
2722
2723 Op::Gather { axis } => {
2724 let table_id = node.inputs[0];
2725 let idx_id = node.inputs[1];
2726 let table_is_param = tensor_is_graph_param(&graph, ¶m_offsets, table_id);
2727 let table_bytes = arena.len_of(table_id) as u64;
2728 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
2737 let gather_needs_split = *axis == 0
2738 && arena_whole_arena_bind(&arena, max_binding).is_none()
2739 && arena_span_bytes(&arena, &[table_id, idx_id, node.id]) > max_binding;
2740 if gather_needs_split {
2741 let table_shape = graph.node(table_id).shape.dims();
2742 let idx_shape = graph.node(idx_id).shape.dims();
2743 let vocab = table_shape[0].unwrap_static() as u32;
2744 let dim: u32 = table_shape[1..]
2745 .iter()
2746 .map(|d| d.unwrap_static() as u32)
2747 .product::<u32>()
2748 .max(1);
2749 let n_idx: u32 =
2750 idx_shape.iter().map(|d| d.unwrap_static() as u32).product();
2751 let table_w = arena.len_of(table_id) as u64;
2752 assert!(
2753 table_w <= max_binding,
2754 "rlx-wgpu gather_split: embedding table {table_w} bytes exceeds \
2755 max_storage_buffer_binding_size {max_binding}"
2756 );
2757 schedule.push(Step::GatherSplit {
2758 n_out: elems,
2759 n_idx,
2760 dim,
2761 vocab,
2762 table_byte_off: arena.offset(table_id) as u64,
2763 idx_byte_off: arena.offset(idx_id) as u64,
2764 out_byte_off: arena.offset(node.id) as u64,
2765 });
2766 continue;
2770 }
2771 let gather_win: Vec<NodeId> = if table_is_param && table_bytes > ARENA_STAGE_CAP
2772 {
2773 vec![table_id, node.id, idx_id]
2774 } else {
2775 vec![node.id, idx_id, table_id]
2776 };
2777 let mut scratch = arena.scratch_off as u64;
2778 let (mut base, mut size, table_anchor) = arena_multi_op_window(
2779 &dev.device,
2780 &arena,
2781 &graph,
2782 ¶m_offsets,
2783 &mut schedule,
2784 &mut scratch,
2785 &gather_win,
2786 );
2787 if !table_anchor {
2788 base = arena_bind_window_covering_scratch_if_needed(
2789 &arena, base, size, scratch,
2790 );
2791 }
2792 let in_off =
2793 if table_anchor && arena_tensor_in_window(&arena, table_id, base, size) {
2794 arena_local_off_f32(&arena, table_id, base)
2795 } else {
2796 arena_off_in_bind_window(
2797 &graph,
2798 ¶m_offsets,
2799 &dev.device,
2800 &arena,
2801 &mut schedule,
2802 &mut scratch,
2803 table_id,
2804 &mut base,
2805 &mut size,
2806 )
2807 };
2808 let idx_off = arena_off_in_bind_window(
2809 &graph,
2810 ¶m_offsets,
2811 &dev.device,
2812 &arena,
2813 &mut schedule,
2814 &mut scratch,
2815 idx_id,
2816 &mut base,
2817 &mut size,
2818 );
2819 let out_off = arena_local_off_f32(&arena, node.id, base);
2820 if *axis == 0 {
2821 let table_shape = graph.node(table_id).shape.dims();
2822 let idx_shape = graph.node(idx_id).shape.dims();
2823 let vocab = table_shape[0].unwrap_static() as u32;
2824 let dim: u32 = table_shape[1..]
2825 .iter()
2826 .map(|d| d.unwrap_static() as u32)
2827 .product::<u32>()
2828 .max(1);
2829 let n_idx: u32 =
2830 idx_shape.iter().map(|d| d.unwrap_static() as u32).product();
2831 let p = GatherParams {
2832 n_out: elems,
2833 n_idx,
2834 dim,
2835 vocab,
2836 in_off,
2837 idx_off,
2838 out_off,
2839 _p0: 0,
2840 };
2841 schedule.push(Step::Gather { params: p });
2842 let gk = gather_kernel(&dev.device);
2843 let u = emit_uniform(std::mem::size_of::<GatherParams>());
2844 let bg =
2845 bind_two_buf0_window(&dev.device, gk, &arena.buffer, base, size, &u);
2846 uniforms.push(u);
2847 bind_groups.push(bg);
2848 } else {
2849 let table_shape = graph.node(table_id).shape.dims();
2850 let idx_shape = graph.node(idx_id).shape.dims();
2851 let outer: u32 = table_shape[..*axis]
2852 .iter()
2853 .map(|d| d.unwrap_static() as u32)
2854 .product::<u32>()
2855 .max(1);
2856 let trailing: u32 = table_shape[*axis + 1..]
2857 .iter()
2858 .map(|d| d.unwrap_static() as u32)
2859 .product::<u32>()
2860 .max(1);
2861 let axis_dim = table_shape[*axis].unwrap_static() as u32;
2862 let num_idx: u32 =
2863 idx_shape.iter().map(|d| d.unwrap_static() as u32).product();
2864 let total = outer * num_idx * trailing;
2865 let p = GatherAxisParams {
2866 total,
2867 outer,
2868 axis_dim,
2869 num_idx,
2870 trailing,
2871 table_off: in_off,
2872 idx_off,
2873 out_off,
2874 };
2875 schedule.push(Step::GatherAxis { params: p });
2876 let gk = gather_axis_kernel(&dev.device);
2877 let u = emit_uniform(std::mem::size_of::<GatherAxisParams>());
2878 let bg =
2879 bind_two_buf0_window(&dev.device, gk, &arena.buffer, base, size, &u);
2880 uniforms.push(u);
2881 bind_groups.push(bg);
2882 }
2883 }
2884
2885 Op::FusedMatMulBiasAct { activation } => {
2886 let a_id = node.inputs[0];
2889 let b_id = node.inputs[1];
2890 let bias_id = node.inputs[2];
2891 let a_shape = graph.node(a_id).shape.dims();
2892 let b_shape = graph.node(b_id).shape.dims();
2893 let out_shape = node.shape.dims();
2894 let (m, k, n) =
2895 if a_shape.len() == 2 && b_shape.len() == 2 && out_shape.len() == 2 {
2896 (
2897 a_shape[0].unwrap_static() as u32,
2898 a_shape[1].unwrap_static() as u32,
2899 b_shape[1].unwrap_static() as u32,
2900 )
2901 } else if a_shape.len() >= 2
2902 && b_shape.len() == 2
2903 && out_shape.len() == a_shape.len()
2904 {
2905 let leading: usize = a_shape[..a_shape.len() - 2]
2906 .iter()
2907 .map(|d| d.unwrap_static())
2908 .product();
2909 let m_inner = a_shape[a_shape.len() - 2].unwrap_static();
2910 let k_inner = a_shape[a_shape.len() - 1].unwrap_static();
2911 let n_inner = b_shape[1].unwrap_static();
2912 ((leading * m_inner) as u32, k_inner as u32, n_inner as u32)
2913 } else {
2914 panic!(
2915 "rlx-wgpu FusedMatMulBiasAct: unsupported shapes \
2916 a={a_shape:?} b={b_shape:?}"
2917 );
2918 };
2919 let act_id = match activation {
2920 None => 0xFFFFu32,
2921 Some(a) => activation_op_id(*a),
2922 };
2923 let b_is_param = tensor_is_graph_param(&graph, ¶m_offsets, b_id);
2924 let b_bytes = arena.len_of(b_id) as u64;
2925 let mut compute_precision = derive_matmul_compute(
2926 &dev.device,
2927 &graph,
2928 &coop_f16_vk_mirror_acts,
2929 a_id,
2930 b_id,
2931 m,
2932 k,
2933 n,
2934 );
2935 if b_is_param
2936 && b_bytes > ARENA_STAGE_CAP
2937 && arena.param_fits_f16_mirror(b_id)
2938 && !rlx_ir::env::flag("RLX_WGPU_NO_F16_MIRROR")
2939 {
2940 compute_precision = MatmulCompute::F16;
2941 }
2942
2943 let mqk_eligible = act_id == 0xFFFFu32
2947 && matches!(
2948 compute_precision,
2949 MatmulCompute::F32 | MatmulCompute::CoopF32 | MatmulCompute::CoopF16Vk
2950 );
2951 if mqk_eligible && let Some(&(q_id, k_id_n, v_id)) = qkv_split.get(&node.id) {
2952 let head_width = n / 3;
2953 let qkv_kind = match compute_precision {
2954 MatmulCompute::CoopF16Vk => MatmulQkvKind::CoopF16Vk,
2955 MatmulCompute::CoopF32 => MatmulQkvKind::CoopF32,
2956 _ => MatmulQkvKind::F32,
2957 };
2958 let b_in_arena = !matmul_b_from_f16(compute_precision, b_is_param);
2959 let (mut base, mut size, param_anchor) = arena_matmul_bind_window(
2960 &dev.device,
2961 &arena,
2962 &graph,
2963 ¶m_offsets,
2964 q_id,
2965 a_id,
2966 b_id,
2967 b_in_arena,
2968 );
2969 let mut scratch = arena.scratch_off as u64;
2970 if param_anchor {
2971 arena_ensure_scratch_in_window(&mut scratch, base, size);
2972 }
2973 if b_is_param && b_bytes > ARENA_STAGE_CAP && b_in_arena {
2974 assert!(
2975 param_anchor && arena_tensor_in_window(&arena, b_id, base, size),
2976 "rlx-wgpu FusedMatMul QKV: large param B {:?} not in bind window",
2977 b_id,
2978 );
2979 }
2980 let a_off = arena_off_in_bind_window(
2981 &graph,
2982 ¶m_offsets,
2983 &dev.device,
2984 &arena,
2985 &mut schedule,
2986 &mut scratch,
2987 a_id,
2988 &mut base,
2989 &mut size,
2990 );
2991 let q_off = arena_off_in_bind_window(
2992 &graph,
2993 ¶m_offsets,
2994 &dev.device,
2995 &arena,
2996 &mut schedule,
2997 &mut scratch,
2998 q_id,
2999 &mut base,
3000 &mut size,
3001 );
3002 let k_off = arena_off_in_bind_window(
3003 &graph,
3004 ¶m_offsets,
3005 &dev.device,
3006 &arena,
3007 &mut schedule,
3008 &mut scratch,
3009 k_id_n,
3010 &mut base,
3011 &mut size,
3012 );
3013 let v_off = arena_off_in_bind_window(
3014 &graph,
3015 ¶m_offsets,
3016 &dev.device,
3017 &arena,
3018 &mut schedule,
3019 &mut scratch,
3020 v_id,
3021 &mut base,
3022 &mut size,
3023 );
3024 let bias_off = arena_off_in_bind_window(
3025 &graph,
3026 ¶m_offsets,
3027 &dev.device,
3028 &arena,
3029 &mut schedule,
3030 &mut scratch,
3031 bias_id,
3032 &mut base,
3033 &mut size,
3034 );
3035 let b_off_f32 = if !b_in_arena {
3036 (arena.offset(b_id) / 4) as u32
3037 } else if b_is_param
3038 && b_bytes > ARENA_STAGE_CAP
3039 && arena_tensor_in_window(&arena, b_id, base, size)
3040 {
3041 arena_local_off_f32(&arena, b_id, base)
3042 } else {
3043 arena_off_in_bind_window(
3044 &graph,
3045 ¶m_offsets,
3046 &dev.device,
3047 &arena,
3048 &mut schedule,
3049 &mut scratch,
3050 b_id,
3051 &mut base,
3052 &mut size,
3053 )
3054 };
3055 let b_off_global = (arena.offset(b_id) / 4) as u32;
3056 maybe_push_coop_f16_vk_casts(
3057 &graph,
3058 a_id,
3059 b_id,
3060 &coop_f16_vk_mirror_acts,
3061 &dev.device,
3062 &arena,
3063 &mut schedule,
3064 &mut uniforms,
3065 &mut bind_groups,
3066 &mm_cast,
3067 compute_precision,
3068 a_off,
3069 m,
3070 k,
3071 1,
3072 if qkv_kind == MatmulQkvKind::CoopF16Vk {
3073 b_off_global
3074 } else {
3075 b_off_f32
3076 },
3077 n,
3078 );
3079 let p = MatmulQkvParams {
3080 m,
3081 k,
3082 n,
3083 a_off,
3084 b_off: if qkv_kind == MatmulQkvKind::CoopF16Vk {
3085 b_off_global
3086 } else {
3087 b_off_f32
3088 },
3089 q_off,
3090 k_off,
3091 v_off,
3092 head_width,
3093 has_bias: 1,
3094 bias_off,
3095 _p0: 0,
3096 _p1: 0,
3097 _p2: 0,
3098 _p3: 0,
3099 _p4: 0,
3100 };
3101 schedule.push(Step::MatmulQkv {
3102 params: p,
3103 kind: qkv_kind,
3104 });
3105 register_coop_f16_vk_b_param(
3106 &mut coop_f16_b_param,
3107 ¶m_offsets,
3108 b_id,
3109 p.b_off,
3110 match qkv_kind {
3111 MatmulQkvKind::CoopF16Vk => MatmulCompute::CoopF16Vk,
3112 MatmulQkvKind::CoopF32 => MatmulCompute::CoopF32,
3113 MatmulQkvKind::F32 => MatmulCompute::F32,
3114 },
3115 );
3116 let u = emit_uniform(std::mem::size_of::<MatmulQkvParams>());
3117 let bg = match qkv_kind {
3118 MatmulQkvKind::CoopF16Vk => {
3119 let mqk = matmul_qkv_coop_f16_vk_kernel(&dev.device).expect(
3120 "coop f16 matmul_qkv kernel: feature was checked but missing",
3121 );
3122 let (bg, b_off_adj) = build_matmul_qkv_coop_f16_vk_bind_group(
3123 &dev.device,
3124 mqk,
3125 &arena,
3126 base,
3127 size,
3128 &u,
3129 k,
3130 n,
3131 p.b_off,
3132 );
3133 if let Some(Step::MatmulQkv { params, .. }) = schedule.last_mut() {
3134 params.b_off = b_off_adj;
3135 }
3136 bg
3137 }
3138 MatmulQkvKind::CoopF32 => bind_two_buf0_window(
3139 &dev.device,
3140 matmul_qkv_coop_f32_kernel(&dev.device).expect(
3141 "coop matmul_qkv kernel: hardware feature was checked but kernel missing",
3142 ),
3143 &arena.buffer,
3144 base,
3145 size,
3146 &u,
3147 ),
3148 MatmulQkvKind::F32 => bind_two_buf0_window(
3149 &dev.device,
3150 matmul_qkv_kernel(&dev.device),
3151 &arena.buffer,
3152 base,
3153 size,
3154 &u,
3155 ),
3156 };
3157 uniforms.push(u);
3158 bind_groups.push(bg);
3159 if qkv_kind == MatmulQkvKind::CoopF16Vk {
3160 coop_f16_vk_wide_bind_groups.insert(
3161 schedule.len() - 1,
3162 bind_two_buf0_window(
3163 &dev.device,
3164 matmul_qkv_kernel(&dev.device),
3165 &arena.buffer,
3166 base,
3167 size,
3168 &uniforms[uniforms.len() - 1],
3169 ),
3170 );
3171 }
3172 } else {
3173 let b_in_arena = !matmul_b_from_f16(compute_precision, b_is_param);
3174 let (mut base, mut size, param_anchor) = arena_matmul_bind_window(
3175 &dev.device,
3176 &arena,
3177 &graph,
3178 ¶m_offsets,
3179 node.id,
3180 a_id,
3181 b_id,
3182 b_in_arena,
3183 );
3184 let mut scratch = arena.scratch_off as u64;
3185 if param_anchor {
3186 arena_ensure_scratch_in_window(&mut scratch, base, size);
3187 }
3188 if b_is_param && b_bytes > ARENA_STAGE_CAP && b_in_arena {
3189 assert!(
3190 param_anchor && arena_tensor_in_window(&arena, b_id, base, size),
3191 "rlx-wgpu FusedMatMul: large param B {:?} not in bind window",
3192 b_id,
3193 );
3194 }
3195 let a_off_f32 = arena_off_in_bind_window(
3196 &graph,
3197 ¶m_offsets,
3198 &dev.device,
3199 &arena,
3200 &mut schedule,
3201 &mut scratch,
3202 a_id,
3203 &mut base,
3204 &mut size,
3205 );
3206 let b_off_f32 = if !b_in_arena {
3207 (arena.offset(b_id) / 4) as u32
3208 } else if b_is_param
3209 && b_bytes > ARENA_STAGE_CAP
3210 && arena_tensor_in_window(&arena, b_id, base, size)
3211 {
3212 arena_local_off_f32(&arena, b_id, base)
3213 } else {
3214 arena_off_in_bind_window(
3215 &graph,
3216 ¶m_offsets,
3217 &dev.device,
3218 &arena,
3219 &mut schedule,
3220 &mut scratch,
3221 b_id,
3222 &mut base,
3223 &mut size,
3224 )
3225 };
3226 let bias_off_f32 = arena_off_in_bind_window(
3227 &graph,
3228 ¶m_offsets,
3229 &dev.device,
3230 &arena,
3231 &mut schedule,
3232 &mut scratch,
3233 bias_id,
3234 &mut base,
3235 &mut size,
3236 );
3237 let b_off_global = (arena.offset(b_id) / 4) as u32;
3238 let b_off_bind = if b_is_param
3239 && matches!(
3240 compute_precision,
3241 MatmulCompute::Coop16
3242 | MatmulCompute::CoopF16Vk
3243 | MatmulCompute::F16
3244 ) {
3245 b_off_global
3246 } else {
3247 b_off_f32
3248 };
3249 maybe_push_coop_f16_vk_casts(
3250 &graph,
3251 a_id,
3252 b_id,
3253 &coop_f16_vk_mirror_acts,
3254 &dev.device,
3255 &arena,
3256 &mut schedule,
3257 &mut uniforms,
3258 &mut bind_groups,
3259 &mm_cast,
3260 compute_precision,
3261 a_off_f32,
3262 m,
3263 k,
3264 1,
3265 b_off_bind,
3266 n,
3267 );
3268 schedule.push(Step::Matmul {
3269 m,
3270 k,
3271 n,
3272 batch: 1,
3273 a_batch_stride: 0,
3274 b_batch_stride: 0,
3275 c_batch_stride: 0,
3276 a_off_f32,
3277 b_off_f32,
3278 c_off_f32: arena_local_off_f32(&arena, node.id, base),
3279 has_bias: 1,
3280 bias_off_f32,
3281 act_id,
3282 b_is_param,
3283 compute_precision,
3284 });
3285 register_coop_f16_vk_b_param(
3286 &mut coop_f16_b_param,
3287 ¶m_offsets,
3288 b_id,
3289 b_off_bind,
3290 compute_precision,
3291 );
3292 let u = emit_uniform(std::mem::size_of::<MatmulParams>());
3293 let (bg, b_off_adj) = build_matmul_bind_group(
3294 &dev.device,
3295 mm_k,
3296 mm_w,
3297 &mm_f16w,
3298 &mm_f16c,
3299 &mm_coop,
3300 &mm_coop_f32,
3301 &arena,
3302 base,
3303 size,
3304 &u,
3305 b_is_param,
3306 compute_precision,
3307 k,
3308 n,
3309 1,
3310 b_off_bind,
3311 0,
3312 );
3313 if let Some(Step::Matmul { b_off_f32, .. }) = schedule.last_mut() {
3314 *b_off_f32 = b_off_adj;
3315 }
3316 uniforms.push(u);
3317 bind_groups.push(bg);
3318 if compute_precision == MatmulCompute::CoopF16Vk {
3319 coop_f16_vk_wide_bind_groups.insert(
3320 schedule.len() - 1,
3321 bind_two_buf0_window(
3322 &dev.device,
3323 mm_w_active_compile,
3324 &arena.buffer,
3325 base,
3326 size,
3327 &uniforms[uniforms.len() - 1],
3328 ),
3329 );
3330 }
3331 }
3332 }
3333
3334 Op::DotGeneral { .. } => {
3335 panic!(
3340 "rlx-wgpu DotGeneral: leaked past unfusion pass — \
3341 check unfuse.rs::expand_dot_general for missing patterns"
3342 );
3343 }
3344
3345 Op::Sample {
3346 top_k,
3347 top_p,
3348 temperature,
3349 seed,
3350 } => {
3351 let in_id = node.inputs[0];
3352 let in_shape = graph.node(in_id).shape.dims();
3353 let inner = in_shape[in_shape.len() - 1].unwrap_static() as u32;
3354 let total: u32 = in_shape.iter().map(|d| d.unwrap_static() as u32).product();
3355 let outer = total / inner.max(1);
3356 let is_greedy = *top_k == 0
3359 && (*top_p - 1.0).abs() < 1e-6
3360 && (*temperature - 1.0).abs() < 1e-6;
3361 if is_greedy {
3362 let p = ArgmaxParams {
3363 outer,
3364 inner,
3365 in_off: (arena.offset(in_id) / 4) as u32,
3366 out_off: (arena.offset(node.id) / 4) as u32,
3367 _p0: 0,
3368 _p1: 0,
3369 _p2: 0,
3370 _p3: 0,
3371 };
3372 schedule.push(Step::Argmax { params: p });
3373 let amk = argmax_kernel(&dev.device);
3374 let u = emit_uniform(std::mem::size_of::<ArgmaxParams>());
3375 let bg = bind_op_output_window(&dev.device, amk, &arena, node.id, &u);
3376 uniforms.push(u);
3377 bind_groups.push(bg);
3378 } else {
3379 let p = SampleParams {
3380 outer,
3381 inner,
3382 in_off: (arena.offset(in_id) / 4) as u32,
3383 out_off: (arena.offset(node.id) / 4) as u32,
3384 top_k: *top_k as u32,
3385 top_p_bits: top_p.to_bits(),
3386 temp_bits: temperature.to_bits(),
3387 seed_lo: *seed as u32,
3388 seed_hi: (*seed >> 32) as u32,
3389 _p0: 0,
3390 _p1: 0,
3391 _p2: 0,
3392 };
3393 schedule.push(Step::Sample { params: p });
3394 let sk = sample_kernel(&dev.device);
3395 let u = emit_uniform(std::mem::size_of::<SampleParams>());
3396 let bg = bind_op_output_window(&dev.device, sk, &arena, node.id, &u);
3397 uniforms.push(u);
3398 bind_groups.push(bg);
3399 }
3400 }
3401
3402 Op::Pool {
3403 kind,
3404 kernel_size,
3405 stride,
3406 padding,
3407 } => {
3408 let in_shape = graph.node(node.inputs[0]).shape.dims();
3409 let out_shape = node.shape.dims();
3410 let op_id: u32 = match kind {
3411 ReduceOp::Sum => 0,
3412 ReduceOp::Mean => 1,
3413 ReduceOp::Max => 2,
3414 ReduceOp::Min => 3,
3415 ReduceOp::Prod => 4,
3416 };
3417 match (kernel_size.len(), in_shape.len(), out_shape.len()) {
3418 (1, 3, 3) => {
3419 let p = Pool1dParams {
3420 n: in_shape[0].unwrap_static() as u32,
3421 c: in_shape[1].unwrap_static() as u32,
3422 l: in_shape[2].unwrap_static() as u32,
3423 l_out: out_shape[2].unwrap_static() as u32,
3424 kl: kernel_size[0] as u32,
3425 sl: stride.first().copied().unwrap_or(1) as u32,
3426 pl: padding.first().copied().unwrap_or(0) as u32,
3427 op: op_id,
3428 in_off: (arena.offset(node.inputs[0]) / 4) as u32,
3429 out_off: (arena.offset(node.id) / 4) as u32,
3430 _p0: 0,
3431 _p1: 0,
3432 _p2: 0,
3433 _p3: 0,
3434 _p4: 0,
3435 _p5: 0,
3436 };
3437 schedule.push(Step::Pool1d { params: p });
3438 let pk = pool1d_kernel(&dev.device);
3439 let u = emit_uniform(std::mem::size_of::<Pool1dParams>());
3440 let bg = bind_op_output_window(&dev.device, pk, &arena, node.id, &u);
3441 uniforms.push(u);
3442 bind_groups.push(bg);
3443 }
3444 (2, 4, 4) => {
3445 let p = Pool2dParams {
3446 n: in_shape[0].unwrap_static() as u32,
3447 c: in_shape[1].unwrap_static() as u32,
3448 h: in_shape[2].unwrap_static() as u32,
3449 w: in_shape[3].unwrap_static() as u32,
3450 h_out: out_shape[2].unwrap_static() as u32,
3451 w_out: out_shape[3].unwrap_static() as u32,
3452 kh: kernel_size[0] as u32,
3453 kw: kernel_size[1] as u32,
3454 sh: stride.first().copied().unwrap_or(1) as u32,
3455 sw: stride.get(1).copied().unwrap_or(1) as u32,
3456 ph: padding.first().copied().unwrap_or(0) as u32,
3457 pw: padding.get(1).copied().unwrap_or(0) as u32,
3458 op: op_id,
3459 in_off: (arena.offset(node.inputs[0]) / 4) as u32,
3460 out_off: (arena.offset(node.id) / 4) as u32,
3461 _p0: 0,
3462 _p1: 0,
3463 _p2: 0,
3464 };
3465 schedule.push(Step::Pool2d { params: p });
3466 let pk = pool2d_kernel(&dev.device);
3467 let u = emit_uniform(std::mem::size_of::<Pool2dParams>());
3468 let bg = bind_op_output_window(&dev.device, pk, &arena, node.id, &u);
3469 uniforms.push(u);
3470 bind_groups.push(bg);
3471 }
3472 (3, 5, 5) => {
3473 let p = Pool3dParams {
3474 n: in_shape[0].unwrap_static() as u32,
3475 c: in_shape[1].unwrap_static() as u32,
3476 d: in_shape[2].unwrap_static() as u32,
3477 h: in_shape[3].unwrap_static() as u32,
3478 w: in_shape[4].unwrap_static() as u32,
3479 d_out: out_shape[2].unwrap_static() as u32,
3480 h_out: out_shape[3].unwrap_static() as u32,
3481 w_out: out_shape[4].unwrap_static() as u32,
3482 kd: kernel_size[0] as u32,
3483 kh: kernel_size[1] as u32,
3484 kw: kernel_size[2] as u32,
3485 sd: stride.first().copied().unwrap_or(1) as u32,
3486 sh: stride.get(1).copied().unwrap_or(1) as u32,
3487 sw: stride.get(2).copied().unwrap_or(1) as u32,
3488 pd: padding.first().copied().unwrap_or(0) as u32,
3489 ph: padding.get(1).copied().unwrap_or(0) as u32,
3490 pw: padding.get(2).copied().unwrap_or(0) as u32,
3491 op: op_id,
3492 in_off: (arena.offset(node.inputs[0]) / 4) as u32,
3493 out_off: (arena.offset(node.id) / 4) as u32,
3494 _p0: 0,
3495 _p1: 0,
3496 };
3497 schedule.push(Step::Pool3d { params: p });
3498 let pk = pool3d_kernel(&dev.device);
3499 let u = emit_uniform(std::mem::size_of::<Pool3dParams>());
3500 let bg = bind_op_output_window(&dev.device, pk, &arena, node.id, &u);
3501 uniforms.push(u);
3502 bind_groups.push(bg);
3503 }
3504 (k, n, m) => panic!(
3505 "rlx-wgpu Pool: kernel-rank {k} with input rank {n} / \
3506 output rank {m} not supported (use 1D/2D/3D NCHW)"
3507 ),
3508 }
3509 }
3510
3511 Op::Conv {
3512 kernel_size,
3513 stride,
3514 padding,
3515 dilation,
3516 groups,
3517 } => {
3518 let in_id = node.inputs[0];
3519 let w_id = node.inputs[1];
3520 let win_ids = [node.id, in_id, w_id];
3521 let max_binding = dev.device.limits().max_storage_buffer_binding_size;
3522 let fits = arena_span_bytes(&arena, &win_ids) <= max_binding;
3523 let mut scratch = arena.scratch_off as u64;
3524 let (mut base, mut size, param_anchor) = arena_multi_op_window(
3525 &dev.device,
3526 &arena,
3527 &graph,
3528 ¶m_offsets,
3529 &mut schedule,
3530 &mut scratch,
3531 &win_ids,
3532 );
3533 arena_expand_bind_window(&arena, &win_ids, &mut base, &mut size, max_binding);
3534 if !fits && !param_anchor {
3535 base = arena_bind_window_covering_scratch_if_needed(
3536 &arena, base, size, scratch,
3537 );
3538 }
3539 let in_off = arena_off_in_bind_window(
3540 &graph,
3541 ¶m_offsets,
3542 &dev.device,
3543 &arena,
3544 &mut schedule,
3545 &mut scratch,
3546 in_id,
3547 &mut base,
3548 &mut size,
3549 );
3550 let w_off = arena_off_in_bind_window(
3551 &graph,
3552 ¶m_offsets,
3553 &dev.device,
3554 &arena,
3555 &mut schedule,
3556 &mut scratch,
3557 w_id,
3558 &mut base,
3559 &mut size,
3560 );
3561 let out_off = arena_off_in_bind_window(
3562 &graph,
3563 ¶m_offsets,
3564 &dev.device,
3565 &arena,
3566 &mut schedule,
3567 &mut scratch,
3568 node.id,
3569 &mut base,
3570 &mut size,
3571 );
3572 let in_shape = graph.node(in_id).shape.dims();
3573 let w_shape = graph.node(w_id).shape.dims();
3574 let out_shape = node.shape.dims();
3575 let s = |i: usize| stride.get(i).copied().unwrap_or(1) as u32;
3576 let p = |i: usize| padding.get(i).copied().unwrap_or(0) as u32;
3577 let d = |i: usize| dilation.get(i).copied().unwrap_or(1) as u32;
3578 match (
3579 kernel_size.len(),
3580 in_shape.len(),
3581 w_shape.len(),
3582 out_shape.len(),
3583 ) {
3584 (1, 3, 3, 3) => {
3585 let p1 = Conv1dParams {
3586 n: in_shape[0].unwrap_static() as u32,
3587 c_in: in_shape[1].unwrap_static() as u32,
3588 c_out: out_shape[1].unwrap_static() as u32,
3589 l: in_shape[2].unwrap_static() as u32,
3590 l_out: out_shape[2].unwrap_static() as u32,
3591 kl: kernel_size[0] as u32,
3592 sl: s(0),
3593 pl: p(0),
3594 dl: d(0),
3595 groups: *groups as u32,
3596 in_off,
3597 w_off,
3598 out_off,
3599 _p0: 0,
3600 _p1: 0,
3601 _p2: 0,
3602 };
3603 schedule.push(Step::Conv1d { params: p1 });
3604 let ck = conv1d_kernel(&dev.device);
3605 let u = emit_uniform(std::mem::size_of::<Conv1dParams>());
3606 let bg = bind_two_buf0_window(
3607 &dev.device,
3608 ck,
3609 &arena.buffer,
3610 base,
3611 size,
3612 &u,
3613 );
3614 uniforms.push(u);
3615 bind_groups.push(bg);
3616 }
3617 (2, 4, 4, 4) => {
3618 let h_in = in_shape[2].unwrap_static() as u32;
3619 let w_in = in_shape[3].unwrap_static() as u32;
3620 let one_d = h_in == 1
3627 && w_in > 1
3628 && kernel_size[0] > 1
3629 && kernel_size.get(1).copied().unwrap_or(1) == 1;
3630 let (h, w, h_out, w_out, kh, kw, sh, sw, ph, pw, dh, dw) = if one_d {
3631 (
3632 w_in,
3633 1,
3634 out_shape[3].unwrap_static() as u32,
3635 1,
3636 kernel_size[0] as u32,
3637 1,
3638 s(0),
3639 1,
3640 p(0),
3641 0,
3642 d(0),
3643 1,
3644 )
3645 } else {
3646 (
3647 h_in,
3648 w_in,
3649 out_shape[2].unwrap_static() as u32,
3650 out_shape[3].unwrap_static() as u32,
3651 kernel_size[0] as u32,
3652 kernel_size[1] as u32,
3653 s(0),
3654 s(1),
3655 p(0),
3656 p(1),
3657 d(0),
3658 d(1),
3659 )
3660 };
3661 let p2 = Conv2dParams {
3662 n: in_shape[0].unwrap_static() as u32,
3663 c_in: in_shape[1].unwrap_static() as u32,
3664 c_out: out_shape[1].unwrap_static() as u32,
3665 h,
3666 w,
3667 h_out,
3668 w_out,
3669 kh,
3670 kw,
3671 sh,
3672 sw,
3673 ph,
3674 pw,
3675 dh,
3676 dw,
3677 groups: *groups as u32,
3678 in_off,
3679 w_off,
3680 out_off,
3681 };
3682 schedule.push(Step::Conv2d { params: p2 });
3683 let ck = conv2d_kernel(&dev.device);
3684 let u = emit_uniform(std::mem::size_of::<Conv2dParams>());
3685 let bg = bind_two_buf0_window(
3686 &dev.device,
3687 ck,
3688 &arena.buffer,
3689 base,
3690 size,
3691 &u,
3692 );
3693 uniforms.push(u);
3694 bind_groups.push(bg);
3695 }
3696 (3, 5, 5, 5) => {
3697 let p3 = Conv3dParams {
3698 n: in_shape[0].unwrap_static() as u32,
3699 c_in: in_shape[1].unwrap_static() as u32,
3700 c_out: out_shape[1].unwrap_static() as u32,
3701 d: in_shape[2].unwrap_static() as u32,
3702 h: in_shape[3].unwrap_static() as u32,
3703 w: in_shape[4].unwrap_static() as u32,
3704 d_out: out_shape[2].unwrap_static() as u32,
3705 h_out: out_shape[3].unwrap_static() as u32,
3706 w_out: out_shape[4].unwrap_static() as u32,
3707 kd: kernel_size[0] as u32,
3708 kh: kernel_size[1] as u32,
3709 kw: kernel_size[2] as u32,
3710 sd: s(0),
3711 sh: s(1),
3712 sw: s(2),
3713 pd: p(0),
3714 ph: p(1),
3715 pw: p(2),
3716 dd: d(0),
3717 dh: d(1),
3718 dw: d(2),
3719 groups: *groups as u32,
3720 in_off,
3721 w_off,
3722 out_off,
3723 _p0: 0,
3724 };
3725 schedule.push(Step::Conv3d { params: p3 });
3726 let ck = conv3d_kernel(&dev.device);
3727 let u = emit_uniform(std::mem::size_of::<Conv3dParams>());
3728 let bg = bind_two_buf0_window(
3729 &dev.device,
3730 ck,
3731 &arena.buffer,
3732 base,
3733 size,
3734 &u,
3735 );
3736 uniforms.push(u);
3737 bind_groups.push(bg);
3738 }
3739 (k, ni, wi, mi) => panic!(
3740 "rlx-wgpu Conv: rank kernel={k} in={ni} weight={wi} out={mi} \
3741 not supported (use 1D/2D/3D NCHW)"
3742 ),
3743 }
3744 }
3745
3746 Op::Im2Col {
3747 kernel_size,
3748 stride,
3749 padding,
3750 dilation,
3751 } => {
3752 let x_shape = &graph.node(node.inputs[0]).shape;
3753 if kernel_size.len() != 2 || x_shape.rank() != 4 {
3754 panic!("rlx-wgpu Im2Col: 2D NCHW only");
3755 }
3756 let n = match x_shape.dim(0) {
3757 rlx_ir::shape::Dim::Static(v) => v as u32,
3758 _ => 0,
3759 };
3760 let c_in = x_shape.dim(1).unwrap_static() as u32;
3761 let h = x_shape.dim(2).unwrap_static() as u32;
3762 let w = x_shape.dim(3).unwrap_static() as u32;
3763 let kh = kernel_size[0] as u32;
3764 let kw = kernel_size[1] as u32;
3765 let sh = stride.first().copied().unwrap_or(1) as u32;
3766 let sw = stride.get(1).copied().unwrap_or(1) as u32;
3767 let ph = padding.first().copied().unwrap_or(0) as u32;
3768 let pw = padding.get(1).copied().unwrap_or(0) as u32;
3769 let dh = dilation.first().copied().unwrap_or(1) as u32;
3770 let dw_dil = dilation.get(1).copied().unwrap_or(1) as u32;
3771 let h_out = rlx_ir::shape::conv2d_spatial_output(
3772 h as usize,
3773 kh as usize,
3774 sh as usize,
3775 ph as usize,
3776 dh as usize,
3777 ) as u32;
3778 let w_out = rlx_ir::shape::conv2d_spatial_output(
3779 w as usize,
3780 kw as usize,
3781 sw as usize,
3782 pw as usize,
3783 dw_dil as usize,
3784 ) as u32;
3785 schedule.push(Step::Im2ColHost {
3786 x_byte_off: arena.offset(node.inputs[0]) as u32,
3787 col_byte_off: arena.offset(node.id) as u32,
3788 n,
3789 c_in,
3790 h,
3791 w,
3792 h_out,
3793 w_out,
3794 kh,
3795 kw,
3796 sh,
3797 sw,
3798 ph,
3799 pw,
3800 dh,
3801 dw_dil,
3802 });
3803 }
3804
3805 Op::Cumsum { axis, exclusive } => {
3806 let in_id = node.inputs[0];
3807 let in_shape = graph.node(in_id).shape.dims();
3808 let last = (in_shape.len() - 1) as i32;
3809 if *axis != -1 && *axis != last {
3810 panic!("rlx-wgpu Cumsum: only last-axis wired (got axis={axis})");
3811 }
3812 let inner = in_shape[in_shape.len() - 1].unwrap_static() as u32;
3813 let total: u32 = in_shape.iter().map(|d| d.unwrap_static() as u32).product();
3814 let outer = total / inner.max(1);
3815 let p = CumsumParams {
3816 outer,
3817 inner,
3818 in_off: (arena.offset(in_id) / 4) as u32,
3819 out_off: (arena.offset(node.id) / 4) as u32,
3820 exclusive: if *exclusive { 1 } else { 0 },
3821 _p0: 0,
3822 _p1: 0,
3823 _p2: 0,
3824 };
3825 schedule.push(Step::Cumsum { params: p });
3826 let ck2 = cumsum_kernel(&dev.device);
3827 let u = emit_uniform(std::mem::size_of::<CumsumParams>());
3828 let bg = bind_op_output_window(&dev.device, ck2, &arena, node.id, &u);
3829 uniforms.push(u);
3830 bind_groups.push(bg);
3831 }
3832 Op::Fft { inverse, norm } => {
3833 let in_id = node.inputs[0];
3834 let in_shape = graph.node(in_id).shape.clone();
3835 let meta = rlx_ir::fft::fft_meta(&in_shape);
3836 let dtype = in_shape.dtype();
3837 assert!(
3843 matches!(dtype, rlx_ir::DType::F32),
3844 "rlx-wgpu Op::Fft: only f32 is supported on the wgpu backend \
3845 (the arena is f32-uniform); got {dtype:?}. Run f64/c64 FFT on CPU."
3846 );
3847 let use_gpu = rlx_ir::fft::gpu_fft_native_eligible(dtype, meta.n_complex)
3848 && meta.n_complex >= 2;
3849 let scale = norm.output_scale(meta.n_complex, *inverse) as f32;
3850 if use_gpu {
3851 schedule.push(Step::FftGpu {
3852 src_off: (arena.offset(in_id) / 4) as u32,
3853 dst_off: (arena.offset(node.id) / 4) as u32,
3854 outer: meta.outer as u32,
3855 n: meta.n_complex as u32,
3856 inverse: if *inverse { 1 } else { 0 },
3857 norm_scale: scale,
3858 });
3859 fft_gpu_steps.push(crate::fft_dispatch::FftGpuResources::new(
3860 &dev.device,
3861 &arena.buffer,
3862 ));
3863 } else {
3864 schedule.push(Step::FftHost {
3865 src_byte_off: arena.offset(in_id) as u32,
3866 dst_byte_off: arena.offset(node.id) as u32,
3867 outer: meta.outer as u32,
3868 n_complex: meta.n_complex as u32,
3869 inverse: *inverse,
3870 norm_tag: norm.tag(),
3871 dtype_tag: fft_dtype_tag(dtype),
3872 });
3873 }
3874 }
3875 Op::WelchPeaks { k, n_segments } => {
3876 let spec_shape = graph.node(node.inputs[0]).shape.clone();
3877 let meta = rlx_ir::audio::welch_peaks_meta(&spec_shape, *k, *n_segments)
3878 .unwrap_or_else(|e| panic!("Op::WelchPeaks: {e}"));
3879 let use_gpu = rlx_ir::audio::welch_peaks_gpu_native_eligible(
3880 &spec_shape,
3881 *k,
3882 *n_segments,
3883 )
3884 .unwrap_or(false);
3885 if use_gpu {
3886 let p = WelchPeaksGpuParams {
3887 spec_off: (arena.offset(node.inputs[0]) / 4) as u32,
3888 dst_off: (arena.offset(node.id) / 4) as u32,
3889 welch_batch: meta.welch_batch as u32,
3890 n_fft: meta.n_fft as u32,
3891 n_segments: meta.n_segments as u32,
3892 k: meta.k as u32,
3893 n_bins: meta.n_bins as u32,
3894 _p0: 0,
3895 _p1: 0,
3896 };
3897 schedule.push(Step::WelchPeaksGpu { params: p });
3898 let wk = welch_peaks_gpu_kernel(&dev.device);
3899 let u = emit_uniform(std::mem::size_of::<WelchPeaksGpuParams>());
3900 let bg = bind_op_output_window(&dev.device, wk, &arena, node.id, &u);
3901 uniforms.push(u);
3902 bind_groups.push(bg);
3903 } else {
3904 schedule.push(Step::WelchPeaksHost {
3905 spec_byte_off: arena.offset(node.inputs[0]) as u32,
3906 dst_byte_off: arena.offset(node.id) as u32,
3907 welch_batch: meta.welch_batch as u32,
3908 n_fft: meta.n_fft as u32,
3909 n_segments: meta.n_segments as u32,
3910 k: meta.k as u32,
3911 });
3912 }
3913 }
3914 Op::LogMel => {
3915 let spec_shape = graph.node(node.inputs[0]).shape.clone();
3916 let filt_shape = graph.node(node.inputs[1]).shape.clone();
3917 let meta = rlx_ir::audio::log_mel_meta(&spec_shape, &filt_shape)
3918 .unwrap_or_else(|e| panic!("Op::LogMel: {e}"));
3919 schedule.push(Step::LogMelHost {
3920 spec_byte_off: arena.offset(node.inputs[0]) as u32,
3921 filt_byte_off: arena.offset(node.inputs[1]) as u32,
3922 dst_byte_off: arena.offset(node.id) as u32,
3923 outer: meta.outer as u32,
3924 n_fft: meta.n_fft as u32,
3925 n_bins: meta.n_bins as u32,
3926 n_mels: meta.n_mels as u32,
3927 });
3928 }
3929 Op::LogMelBackward => {
3930 let spec_shape = graph.node(node.inputs[0]).shape.clone();
3931 let filt_shape = graph.node(node.inputs[1]).shape.clone();
3932 let meta = rlx_ir::audio::log_mel_meta(&spec_shape, &filt_shape)
3933 .unwrap_or_else(|e| panic!("Op::LogMelBackward: {e}"));
3934 schedule.push(Step::LogMelBackwardHost {
3935 spec_byte_off: arena.offset(node.inputs[0]) as u32,
3936 filt_byte_off: arena.offset(node.inputs[1]) as u32,
3937 dy_byte_off: arena.offset(node.inputs[2]) as u32,
3938 dst_byte_off: arena.offset(node.id) as u32,
3939 outer: meta.outer as u32,
3940 n_fft: meta.n_fft as u32,
3941 n_bins: meta.n_bins as u32,
3942 n_mels: meta.n_mels as u32,
3943 });
3944 }
3945 Op::SelectiveScan { state_size } => {
3946 if *state_size > 256 {
3947 panic!(
3948 "rlx-wgpu SelectiveScan: state_size {} exceeds compile-time \
3949 cap of 256 (kernel uses fixed-size private array)",
3950 state_size
3951 );
3952 }
3953 let x_id = node.inputs[0];
3954 let dt_id = node.inputs[1];
3955 let a_id = node.inputs[2];
3956 let b_id = node.inputs[3];
3957 let c_id = node.inputs[4];
3958 let in_dims = graph.node(x_id).shape.dims();
3959 let seq = in_dims[1].unwrap_static() as u32;
3960 let p = SelectiveScanParams {
3961 batch: in_dims[0].unwrap_static() as u32,
3962 seq,
3963 hidden: in_dims[2].unwrap_static() as u32,
3964 state_size: *state_size as u32,
3965 x_off: (arena.offset(x_id) / 4) as u32,
3966 delta_off: (arena.offset(dt_id) / 4) as u32,
3967 a_off: (arena.offset(a_id) / 4) as u32,
3968 b_off: (arena.offset(b_id) / 4) as u32,
3969 c_off: (arena.offset(c_id) / 4) as u32,
3970 out_off: (arena.offset(node.id) / 4) as u32,
3971 seq_stride: seq,
3974 _p1: 0,
3975 _p2: 0,
3976 _p3: 0,
3977 _p4: 0,
3978 _p5: 0,
3979 };
3980 schedule.push(Step::SelectiveScan { params: p });
3981 let ssk = selective_scan_kernel(&dev.device);
3982 let u = emit_uniform(std::mem::size_of::<SelectiveScanParams>());
3983 let bg = bind_op_output_window(&dev.device, ssk, &arena, node.id, &u);
3984 uniforms.push(u);
3985 bind_groups.push(bg);
3986 }
3987 Op::Mamba2 {
3988 head_dim,
3989 state_size,
3990 } => {
3991 if *state_size > 256 {
3992 panic!(
3993 "rlx-wgpu Mamba2: state_size {} exceeds compile-time cap of 256",
3994 state_size
3995 );
3996 }
3997 let x_id = node.inputs[0];
3998 let in_dims = graph.node(x_id).shape.dims(); let seq = in_dims[1].unwrap_static() as u32;
4000 let p = Mamba2Params {
4001 batch: in_dims[0].unwrap_static() as u32,
4002 seq,
4003 heads: in_dims[2].unwrap_static() as u32,
4004 head_dim: *head_dim as u32,
4005 state_size: *state_size as u32,
4006 x_off: (arena.offset(x_id) / 4) as u32,
4007 dt_off: (arena.offset(node.inputs[1]) / 4) as u32,
4008 a_off: (arena.offset(node.inputs[2]) / 4) as u32,
4009 b_off: (arena.offset(node.inputs[3]) / 4) as u32,
4010 c_off: (arena.offset(node.inputs[4]) / 4) as u32,
4011 out_off: (arena.offset(node.id) / 4) as u32,
4012 seq_stride: seq,
4013 _p1: 0,
4014 _p2: 0,
4015 _p3: 0,
4016 _p4: 0,
4017 };
4018 schedule.push(Step::Mamba2 { params: p });
4019 let mk = mamba2_kernel(&dev.device);
4020 let u = emit_uniform(std::mem::size_of::<Mamba2Params>());
4021 let bg = bind_op_output_window(&dev.device, mk, &arena, node.id, &u);
4022 uniforms.push(u);
4023 bind_groups.push(bg);
4024 }
4025 Op::Gru {
4026 hidden_size,
4027 num_layers,
4028 bidirectional,
4029 carry,
4030 } => {
4031 let x_id = node.inputs[0];
4032 let in_dims = graph.node(x_id).shape.dims(); let batch = in_dims[0].unwrap_static() as u32;
4034 let seq = in_dims[1].unwrap_static() as u32;
4035 let input_size = in_dims[2].unwrap_static() as u32;
4036 let hidden = *hidden_size as u32;
4037 let simple = *num_layers == 1 && !*bidirectional && !*carry;
4038 if simple && hidden <= 256 {
4039 let p = GruParams {
4040 batch,
4041 seq,
4042 input_size,
4043 hidden,
4044 x_off: (arena.offset(x_id) / 4) as u32,
4045 wih_off: (arena.offset(node.inputs[1]) / 4) as u32,
4046 whh_off: (arena.offset(node.inputs[2]) / 4) as u32,
4047 bih_off: (arena.offset(node.inputs[3]) / 4) as u32,
4048 bhh_off: (arena.offset(node.inputs[4]) / 4) as u32,
4049 out_off: (arena.offset(node.id) / 4) as u32,
4050 seq_stride: seq,
4051 _p1: 0,
4052 _p2: 0,
4053 _p3: 0,
4054 _p4: 0,
4055 _p5: 0,
4056 };
4057 schedule.push(Step::Gru { params: p });
4058 let gk = gru_kernel(&dev.device);
4059 let u = emit_uniform(std::mem::size_of::<GruParams>());
4060 let bg = bind_op_output_window(&dev.device, gk, &arena, node.id, &u);
4061 uniforms.push(u);
4062 bind_groups.push(bg);
4063 } else {
4064 let h0 = if *carry {
4065 arena.offset(node.inputs[5]) as u32
4066 } else {
4067 0
4068 };
4069 schedule.push(Step::GruHost {
4070 x: arena.offset(x_id) as u32,
4071 w_ih: arena.offset(node.inputs[1]) as u32,
4072 w_hh: arena.offset(node.inputs[2]) as u32,
4073 b_ih: arena.offset(node.inputs[3]) as u32,
4074 b_hh: arena.offset(node.inputs[4]) as u32,
4075 h0,
4076 dst: arena.offset(node.id) as u32,
4077 batch,
4078 seq,
4079 input_size,
4080 hidden,
4081 num_layers: *num_layers as u32,
4082 bidirectional: *bidirectional,
4083 carry: *carry,
4084 });
4085 }
4086 }
4087 Op::Rnn {
4088 hidden_size,
4089 num_layers,
4090 bidirectional,
4091 carry,
4092 relu,
4093 } => {
4094 let x_id = node.inputs[0];
4095 let in_dims = graph.node(x_id).shape.dims();
4096 let batch = in_dims[0].unwrap_static() as u32;
4097 let seq = in_dims[1].unwrap_static() as u32;
4098 let input_size = in_dims[2].unwrap_static() as u32;
4099 let hidden = *hidden_size as u32;
4100 let simple = *num_layers == 1 && !*bidirectional && !*carry;
4101 if simple && hidden <= 256 {
4102 let p = RnnParams {
4103 batch,
4104 seq,
4105 input_size,
4106 hidden,
4107 x_off: (arena.offset(x_id) / 4) as u32,
4108 wih_off: (arena.offset(node.inputs[1]) / 4) as u32,
4109 whh_off: (arena.offset(node.inputs[2]) / 4) as u32,
4110 bias_off: (arena.offset(node.inputs[3]) / 4) as u32,
4111 out_off: (arena.offset(node.id) / 4) as u32,
4112 seq_stride: seq,
4113 relu: u32::from(*relu),
4114 _p1: 0,
4115 _p2: 0,
4116 _p3: 0,
4117 _p4: 0,
4118 _p5: 0,
4119 };
4120 schedule.push(Step::Rnn { params: p });
4121 let rk = rnn_kernel(&dev.device);
4122 let u = emit_uniform(std::mem::size_of::<RnnParams>());
4123 let bg = bind_op_output_window(&dev.device, rk, &arena, node.id, &u);
4124 uniforms.push(u);
4125 bind_groups.push(bg);
4126 } else {
4127 let h0 = if *carry {
4128 arena.offset(node.inputs[4]) as u32
4129 } else {
4130 0
4131 };
4132 schedule.push(Step::RnnHost {
4133 x: arena.offset(x_id) as u32,
4134 w_ih: arena.offset(node.inputs[1]) as u32,
4135 w_hh: arena.offset(node.inputs[2]) as u32,
4136 bias: arena.offset(node.inputs[3]) as u32,
4137 h0,
4138 dst: arena.offset(node.id) as u32,
4139 batch,
4140 seq,
4141 input_size,
4142 hidden,
4143 num_layers: *num_layers as u32,
4144 bidirectional: *bidirectional,
4145 carry: *carry,
4146 relu: *relu,
4147 });
4148 }
4149 }
4150 Op::GatedDeltaNet {
4151 state_size,
4152 carry_state,
4153 } => {
4154 if *state_size > rlx_cpu::gdn::GDN_MAX_STATE {
4155 panic!(
4156 "rlx-wgpu GatedDeltaNet: state_size {state_size} > {}",
4157 rlx_cpu::gdn::GDN_MAX_STATE
4158 );
4159 }
4160 let q_id = node.inputs[0];
4161 let q_shape = &graph.node(q_id).shape;
4162 let state_off = if *carry_state {
4163 arena.offset(node.inputs[5])
4164 } else {
4165 0
4166 };
4167 schedule.push(Step::GatedDeltaNet {
4168 q_byte_off: arena.offset(q_id) as u32,
4169 k_byte_off: arena.offset(node.inputs[1]) as u32,
4170 v_byte_off: arena.offset(node.inputs[2]) as u32,
4171 g_byte_off: arena.offset(node.inputs[3]) as u32,
4172 beta_byte_off: arena.offset(node.inputs[4]) as u32,
4173 state_byte_off: state_off as u32,
4174 dst_byte_off: arena.offset(node.id) as u32,
4175 batch: q_shape.dim(0).unwrap_static() as u32,
4176 seq: q_shape.dim(1).unwrap_static() as u32,
4177 heads: q_shape.dim(2).unwrap_static() as u32,
4178 state_size: *state_size as u32,
4179 use_carry: *carry_state,
4180 });
4181 if gguf_host_pad.is_none() {
4182 let bk = binary_kernel(&dev.device);
4183 let u = emit_uniform(256);
4184 gguf_host_pad = Some((
4185 u.clone(),
4186 bind_op_output_window(&dev.device, bk, &arena, node.id, &u),
4187 ));
4188 }
4189 let (u, bg) = gguf_host_pad.as_ref().unwrap();
4190 uniforms.push(u.clone());
4191 bind_groups.push(bg.clone());
4192 }
4193 Op::Lstm {
4194 hidden_size,
4195 num_layers,
4196 bidirectional,
4197 carry,
4198 } => {
4199 let x_shape = &graph.node(node.inputs[0]).shape;
4200 let (h0, c0) = if *carry {
4201 (
4202 arena.offset(node.inputs[4]) as u32,
4203 arena.offset(node.inputs[5]) as u32,
4204 )
4205 } else {
4206 (0u32, 0u32)
4207 };
4208 schedule.push(Step::Lstm {
4209 x_byte_off: arena.offset(node.inputs[0]) as u32,
4210 w_ih_byte_off: arena.offset(node.inputs[1]) as u32,
4211 w_hh_byte_off: arena.offset(node.inputs[2]) as u32,
4212 bias_byte_off: arena.offset(node.inputs[3]) as u32,
4213 h0_byte_off: h0,
4214 c0_byte_off: c0,
4215 dst_byte_off: arena.offset(node.id) as u32,
4216 batch: x_shape.dim(0).unwrap_static() as u32,
4217 seq: x_shape.dim(1).unwrap_static() as u32,
4218 input_size: x_shape.dim(2).unwrap_static() as u32,
4219 hidden: *hidden_size as u32,
4220 num_layers: *num_layers as u32,
4221 bidirectional: *bidirectional,
4222 carry: *carry,
4223 });
4224 if gguf_host_pad.is_none() {
4226 let bk = binary_kernel(&dev.device);
4227 let u = emit_uniform(256);
4228 gguf_host_pad = Some((
4229 u.clone(),
4230 bind_op_output_window(&dev.device, bk, &arena, node.id, &u),
4231 ));
4232 }
4233 let (u, bg) = gguf_host_pad.as_ref().unwrap();
4234 uniforms.push(u.clone());
4235 bind_groups.push(bg.clone());
4236 }
4237 Op::ConvTranspose2d {
4238 kernel_size,
4239 stride,
4240 padding,
4241 dilation,
4242 output_padding: _,
4243 groups,
4244 } => {
4245 let in_shape = &graph.node(node.inputs[0]).shape;
4246 let out_shape = &node.shape;
4247 schedule.push(Step::ConvTranspose2d {
4248 src_byte_off: arena.offset(node.inputs[0]) as u32,
4249 weight_byte_off: arena.offset(node.inputs[1]) as u32,
4250 dst_byte_off: arena.offset(node.id) as u32,
4251 n: in_shape.dim(0).unwrap_static() as u32,
4252 c_in: in_shape.dim(1).unwrap_static() as u32,
4253 h: in_shape.dim(2).unwrap_static() as u32,
4254 w_in: in_shape.dim(3).unwrap_static() as u32,
4255 c_out: out_shape.dim(1).unwrap_static() as u32,
4256 h_out: out_shape.dim(2).unwrap_static() as u32,
4257 w_out: out_shape.dim(3).unwrap_static() as u32,
4258 kh: kernel_size[0] as u32,
4259 kw: kernel_size[1] as u32,
4260 sh: stride[0] as u32,
4261 sw: stride[1] as u32,
4262 ph: padding[0] as u32,
4263 pw: padding[1] as u32,
4264 dh: dilation[0] as u32,
4265 dw: dilation[1] as u32,
4266 groups: *groups as u32,
4267 });
4268 }
4272 Op::GroupNorm { num_groups, eps } => {
4273 let in_shape = &graph.node(node.inputs[0]).shape;
4275 schedule.push(Step::GroupNormHost {
4276 src_byte_off: arena.offset(node.inputs[0]) as u32,
4277 gamma_byte_off: arena.offset(node.inputs[1]) as u32,
4278 beta_byte_off: arena.offset(node.inputs[2]) as u32,
4279 dst_byte_off: arena.offset(node.id) as u32,
4280 n: in_shape.dim(0).unwrap_static() as u32,
4281 c: in_shape.dim(1).unwrap_static() as u32,
4282 h: in_shape.dim(2).unwrap_static() as u32,
4283 w: in_shape.dim(3).unwrap_static() as u32,
4284 num_groups: *num_groups as u32,
4285 eps: *eps,
4286 });
4287 }
4288 Op::LayerNorm2d { eps } => {
4289 let in_shape = &graph.node(node.inputs[0]).shape;
4291 schedule.push(Step::LayerNorm2dHost {
4292 src_byte_off: arena.offset(node.inputs[0]) as u32,
4293 gamma_byte_off: arena.offset(node.inputs[1]) as u32,
4294 beta_byte_off: arena.offset(node.inputs[2]) as u32,
4295 dst_byte_off: arena.offset(node.id) as u32,
4296 n: in_shape.dim(0).unwrap_static() as u32,
4297 c: in_shape.dim(1).unwrap_static() as u32,
4298 h: in_shape.dim(2).unwrap_static() as u32,
4299 w: in_shape.dim(3).unwrap_static() as u32,
4300 eps: *eps,
4301 });
4302 }
4303 Op::ResizeNearest2x => {
4304 let in_shape = &graph.node(node.inputs[0]).shape;
4306 schedule.push(Step::ResizeNearest2xHost {
4307 src_byte_off: arena.offset(node.inputs[0]) as u32,
4308 dst_byte_off: arena.offset(node.id) as u32,
4309 n: in_shape.dim(0).unwrap_static() as u32,
4310 c: in_shape.dim(1).unwrap_static() as u32,
4311 h: in_shape.dim(2).unwrap_static() as u32,
4312 w: in_shape.dim(3).unwrap_static() as u32,
4313 });
4314 }
4315 Op::Reverse { axes } => {
4316 let in_shape = &graph.node(node.inputs[0]).shape;
4318 let rank = in_shape.rank();
4319 let dims: Vec<u32> = (0..rank)
4320 .map(|i| in_shape.dim(i).unwrap_static() as u32)
4321 .collect();
4322 let mut rev_mask = vec![false; rank];
4323 for &a in axes {
4324 if a < rank {
4325 rev_mask[a] = true;
4326 }
4327 }
4328 schedule.push(Step::ReverseHost {
4329 src_byte_off: arena.offset(node.inputs[0]) as u32,
4330 dst_byte_off: arena.offset(node.id) as u32,
4331 dims,
4332 rev_mask,
4333 elem_bytes: in_shape.dtype().size_bytes() as u32,
4334 });
4335 }
4336 Op::ArgMax { axis, keep_dim: _ } | Op::ArgMin { axis, keep_dim: _ } => {
4337 let in_shape = &graph.node(node.inputs[0]).shape;
4339 let rank = in_shape.rank();
4340 let outer: usize = (0..*axis)
4341 .map(|i| in_shape.dim(i).unwrap_static())
4342 .product::<usize>()
4343 .max(1);
4344 let reduced = in_shape.dim(*axis).unwrap_static();
4345 let inner: usize = (*axis + 1..rank)
4346 .map(|i| in_shape.dim(i).unwrap_static())
4347 .product::<usize>()
4348 .max(1);
4349 schedule.push(Step::ArgReduceHost {
4350 src_byte_off: arena.offset(node.inputs[0]) as u32,
4351 dst_byte_off: arena.offset(node.id) as u32,
4352 outer: outer as u32,
4353 reduced: reduced as u32,
4354 inner: inner as u32,
4355 is_max: matches!(node.op, Op::ArgMax { .. }),
4356 });
4357 }
4358 Op::Scan {
4359 body,
4360 length,
4361 save_trajectory,
4362 num_bcast,
4363 num_xs,
4364 ..
4365 } => {
4366 let nb = *num_bcast as usize;
4369 let nx = *num_xs as usize;
4370 let plan = rlx_cpu::thunk::compile_scan_body(body, nb, nx);
4371 let bcast_outer: Vec<(usize, usize)> = (0..nb)
4372 .map(|i| {
4373 let id = node.inputs[1 + i];
4374 (arena.offset(id), graph.node(id).shape.size_bytes().unwrap())
4375 })
4376 .collect();
4377 let xs_outer: Vec<(usize, usize)> = (0..nx)
4378 .map(|i| {
4379 let id = node.inputs[1 + nb + i];
4380 let total = graph.node(id).shape.size_bytes().unwrap();
4381 (arena.offset(id), total / *length as usize)
4382 })
4383 .collect();
4384 schedule.push(Step::ScanHost {
4385 plan: std::sync::Arc::new(plan),
4386 outer_init_off: arena.offset(node.inputs[0]),
4387 outer_final_off: arena.offset(node.id),
4388 length: *length,
4389 save_trajectory: *save_trajectory,
4390 xs_outer,
4391 bcast_outer,
4392 });
4393 }
4394 Op::Custom { name, attrs, .. } => match name.as_str() {
4395 "llada2.group_limited_gate" => {
4396 let sig_id = node.inputs[0];
4397 let route_id = node.inputs[1];
4398 let n_elems = graph.node(sig_id).shape.num_elements().unwrap() as u32;
4399 let mut attr_buf = [0u8; 20];
4400 let n = attrs.len().min(20);
4401 attr_buf[..n].copy_from_slice(&attrs[..n]);
4402 schedule.push(Step::Llada2GroupLimitedGate {
4403 sig_byte_off: arena.offset(sig_id) as u32,
4404 route_byte_off: arena.offset(route_id) as u32,
4405 out_byte_off: arena.offset(node.id) as u32,
4406 n_elems,
4407 attrs: attr_buf,
4408 });
4409 }
4410 "umap.knn" => {
4411 let pw_id = node.inputs[0];
4412 let pw_shape = graph.node(pw_id).shape.dims();
4413 let n = pw_shape[0].unwrap_static() as u32;
4414 let k = if attrs.len() >= 4 {
4415 u32::from_le_bytes(attrs[..4].try_into().unwrap())
4416 } else {
4417 panic!("rlx-wgpu: umap.knn attrs missing k");
4418 };
4419 let pw_off = arena.offset(pw_id) as u32;
4420 let out_off = arena.offset(node.id) as u32;
4421 if n as usize >= crate::umap_knn_host::UMAP_KNN_GPU_MIN_N {
4422 let p = UmapKnnParams {
4423 n,
4424 k,
4425 pw_off: pw_off / 4,
4426 out_off: out_off / 4,
4427 _p0: 0,
4428 _p1: 0,
4429 _p2: 0,
4430 };
4431 schedule.push(Step::UmapKnn { params: p });
4432 let uk = umap_knn_kernel(&dev.device);
4433 let u = emit_uniform(std::mem::size_of::<UmapKnnParams>());
4434 let bg = bind_op_output_window(&dev.device, uk, &arena, node.id, &u);
4435 uniforms.push(u);
4436 bind_groups.push(bg);
4437 } else {
4438 schedule.push(Step::UmapKnnHost {
4439 pairwise_byte_off: pw_off,
4440 out_byte_off: out_off,
4441 n,
4442 k,
4443 });
4444 }
4445 }
4446 "gdino.ms_deform_attn" => {
4447 let in_offs: Vec<(u32, u32)> = node
4448 .inputs
4449 .iter()
4450 .map(|&id| {
4451 let bytes = graph.node(id).shape.num_elements().unwrap() * 4;
4452 (arena.offset(id) as u32, bytes as u32)
4453 })
4454 .collect();
4455 let out_bytes = (node.shape.num_elements().unwrap() * 4) as u32;
4456 schedule.push(Step::MsDeformAttnHost {
4457 in_offs,
4458 out_byte_off: arena.offset(node.id) as u32,
4459 out_bytes,
4460 attrs: attrs.clone(),
4461 });
4462 }
4463 other => panic!("rlx-wgpu: unsupported Op::Custom('{other}')"),
4464 },
4465 Op::GroupedMatMul => {
4466 let in_id = node.inputs[0];
4468 let w_id = node.inputs[1];
4469 let idx_id = node.inputs[2];
4470 let in_dims = graph.node(in_id).shape.dims();
4471 let w_dims = graph.node(w_id).shape.dims();
4472 let m = in_dims[0].unwrap_static() as u32;
4473 let k = in_dims[1].unwrap_static() as u32;
4474 let n = w_dims[2].unwrap_static() as u32;
4475 let ne = w_dims[0].unwrap_static() as u32;
4476 let p = GroupedMatmulParams {
4477 m,
4478 k,
4479 n,
4480 num_experts: ne,
4481 in_off: (arena.offset(in_id) / 4) as u32,
4482 w_off: (arena.offset(w_id) / 4) as u32,
4483 idx_off: (arena.offset(idx_id) / 4) as u32,
4484 out_off: (arena.offset(node.id) / 4) as u32,
4485 };
4486 schedule.push(Step::GroupedMatmul { params: p });
4487 let gk = grouped_matmul_kernel(&dev.device);
4488 let u = emit_uniform(std::mem::size_of::<GroupedMatmulParams>());
4489 let bg = bind_op_output_window(&dev.device, gk, &arena, node.id, &u);
4490 uniforms.push(u);
4491 bind_groups.push(bg);
4492 }
4493 Op::DequantGroupedMatMul { scheme } => {
4494 let in_id = node.inputs[0];
4495 let w_id = node.inputs[1];
4496 let idx_id = node.inputs[2];
4497 let in_dims = graph.node(in_id).shape.dims();
4498 let out_dims = node.shape.dims();
4499 let m = in_dims[0].unwrap_static() as u32;
4500 let k = in_dims[1].unwrap_static() as u32;
4501 let n = out_dims[out_dims.len() - 1].unwrap_static() as u32;
4502 let block_elems = scheme.gguf_block_size() as usize;
4503 let block_bytes = scheme.gguf_block_bytes() as usize;
4504 let slab_bytes = (k as usize * n as usize) / block_elems * block_bytes;
4505 let total_bytes = graph.node(w_id).shape.num_elements().unwrap();
4506 let ne = (total_bytes / slab_bytes.max(1)) as u32;
4507 schedule.push(Step::DequantGroupedMatmulGguf {
4508 m,
4509 k,
4510 n,
4511 num_experts: ne,
4512 scheme_id: crate::gguf_host::gguf_scheme_id(*scheme),
4513 x_byte_off: arena.offset(in_id) as u64,
4514 w_byte_off: arena.offset(w_id) as u64,
4515 idx_byte_off: arena.offset(idx_id) as u64,
4516 out_byte_off: arena.offset(node.id) as u64,
4517 });
4518 }
4523 Op::TopK { k } => {
4524 let in_id = node.inputs[0];
4525 let in_dims = graph.node(in_id).shape.dims();
4526 let inner = in_dims.last().unwrap().unwrap_static() as u32;
4527 let outer: u32 = in_dims[..in_dims.len() - 1]
4528 .iter()
4529 .map(|d| d.unwrap_static() as u32)
4530 .product::<u32>()
4531 .max(1);
4532 let p = TopKParams {
4533 outer,
4534 inner,
4535 k: *k as u32,
4536 in_off: (arena.offset(in_id) / 4) as u32,
4537 out_off: (arena.offset(node.id) / 4) as u32,
4538 _p0: 0,
4539 _p1: 0,
4540 _p2: 0,
4541 };
4542 schedule.push(Step::TopK { params: p });
4543 let tk = topk_kernel(&dev.device);
4544 let u = emit_uniform(std::mem::size_of::<TopKParams>());
4545 let bg = bind_op_output_window(&dev.device, tk, &arena, node.id, &u);
4546 uniforms.push(u);
4547 bind_groups.push(bg);
4548 }
4549 Op::ScatterAdd => {
4550 let upd_id = node.inputs[0];
4555 let idx_id = node.inputs[1];
4556 let upd_dims = graph.node(upd_id).shape.dims();
4557 let out_dims = node.shape.dims();
4558 let num_updates = upd_dims[0].unwrap_static() as u32;
4559 let trailing: u32 = upd_dims
4560 .iter()
4561 .skip(1)
4562 .map(|d| d.unwrap_static() as u32)
4563 .product::<u32>()
4564 .max(1);
4565 let out_dim = out_dims[0].unwrap_static() as u32;
4566 let out_total = out_dim * trailing;
4567
4568 let common = ScatterAddParams {
4569 op: 0,
4570 out_off: (arena.offset(node.id) / 4) as u32,
4571 upd_off: (arena.offset(upd_id) / 4) as u32,
4572 idx_off: (arena.offset(idx_id) / 4) as u32,
4573 out_total,
4574 num_updates,
4575 trailing,
4576 out_dim,
4577 };
4578 let sk = scatter_add_kernel(&dev.device);
4579
4580 schedule.push(Step::ScatterAdd { params: common });
4582 let u0 = emit_uniform(std::mem::size_of::<ScatterAddParams>());
4583 let bg0 = bind_op_output_window(&dev.device, sk, &arena, node.id, &u0);
4584 uniforms.push(u0);
4585 bind_groups.push(bg0);
4586
4587 let mut acc = common;
4589 acc.op = 1;
4590 schedule.push(Step::ScatterAdd { params: acc });
4591 let u1 = emit_uniform(std::mem::size_of::<ScatterAddParams>());
4592 let bg1 = bind_op_output_window(&dev.device, sk, &arena, node.id, &u1);
4593 uniforms.push(u1);
4594 bind_groups.push(bg1);
4595 }
4596 Op::FusedResidualLN { has_bias, eps } => {
4597 let x_id = node.inputs[0];
4599 let r_id = node.inputs[1];
4600 let (bias_id, g_id, b_id) = if *has_bias {
4601 (node.inputs[2], node.inputs[3], node.inputs[4])
4602 } else {
4603 (x_id, node.inputs[2], node.inputs[3]) };
4605 let in_dims = node.shape.dims();
4606 let inner = in_dims[in_dims.len() - 1].unwrap_static() as u32;
4607 let total: u32 = in_dims.iter().map(|d| d.unwrap_static() as u32).product();
4608 let outer = total / inner.max(1);
4609 let p = FusedResidualLnParams {
4610 outer,
4611 inner,
4612 in_off: (arena.offset(x_id) / 4) as u32,
4613 residual_off: (arena.offset(r_id) / 4) as u32,
4614 bias_off: (arena.offset(bias_id) / 4) as u32,
4615 gamma_off: (arena.offset(g_id) / 4) as u32,
4616 beta_off: (arena.offset(b_id) / 4) as u32,
4617 out_off: (arena.offset(node.id) / 4) as u32,
4618 eps_bits: eps.to_bits(),
4619 has_bias: if *has_bias { 1 } else { 0 },
4620 _p0: 0,
4621 _p1: 0,
4622 };
4623 schedule.push(Step::FusedResidualLn { params: p });
4624 let frk = fused_residual_ln_kernel(&dev.device);
4625 let u = emit_uniform(std::mem::size_of::<FusedResidualLnParams>());
4626 let bg = bind_op_output_window(&dev.device, frk, &arena, node.id, &u);
4627 uniforms.push(u);
4628 bind_groups.push(bg);
4629 }
4630 Op::FusedResidualRmsNorm { has_bias, eps } => {
4631 let x_id = node.inputs[0];
4632 let r_id = node.inputs[1];
4633 let (bias_id, g_id, b_id) = if *has_bias {
4634 (node.inputs[2], node.inputs[3], node.inputs[4])
4635 } else {
4636 (x_id, node.inputs[2], node.inputs[3])
4637 };
4638 let in_dims = node.shape.dims();
4639 let inner = in_dims[in_dims.len() - 1].unwrap_static() as u32;
4640 let total: u32 = in_dims.iter().map(|d| d.unwrap_static() as u32).product();
4641 let outer = total / inner.max(1);
4642 let p = FusedResidualRmsNormParams {
4643 outer,
4644 inner,
4645 in_off: (arena.offset(x_id) / 4) as u32,
4646 residual_off: (arena.offset(r_id) / 4) as u32,
4647 bias_off: (arena.offset(bias_id) / 4) as u32,
4648 gamma_off: (arena.offset(g_id) / 4) as u32,
4649 beta_off: (arena.offset(b_id) / 4) as u32,
4650 out_off: (arena.offset(node.id) / 4) as u32,
4651 eps_bits: eps.to_bits(),
4652 has_bias: if *has_bias { 1 } else { 0 },
4653 _p0: 0,
4654 _p1: 0,
4655 };
4656 schedule.push(Step::FusedResidualRmsNorm { params: p });
4657 let frk = fused_residual_rms_norm_kernel(&dev.device);
4658 let u = emit_uniform(std::mem::size_of::<FusedResidualRmsNormParams>());
4659 let bg = bind_op_output_window(&dev.device, frk, &arena, node.id, &u);
4660 uniforms.push(u);
4661 bind_groups.push(bg);
4662 }
4663 Op::DequantMatMul { scheme } => {
4664 use rlx_ir::QuantScheme;
4665 let x_id = node.inputs[0];
4666 let w_id = node.inputs[1];
4667 let out_total = node.shape.num_elements().unwrap_or(0) as u32;
4674 let n = node.shape.dim(node.shape.rank() - 1).unwrap_static() as u32;
4675 let m = out_total / n.max(1);
4676 let x_total = graph.node(x_id).shape.num_elements().unwrap_or(0) as u32;
4677 let k = x_total / m.max(1);
4678 if scheme.is_gguf() {
4679 schedule.push(Step::DequantMatmulGguf {
4680 m,
4681 k,
4682 n,
4683 scheme_id: crate::gguf_host::gguf_scheme_id(*scheme),
4684 x_byte_off: arena.offset(x_id) as u64,
4685 w_byte_off: arena.offset(w_id) as u64,
4686 out_byte_off: arena.offset(node.id) as u64,
4687 });
4688 } else {
4696 let (block_size, scheme_id) = match scheme {
4697 QuantScheme::Int8Block { block_size } => (*block_size, 0u32),
4698 QuantScheme::Int8BlockAsym { block_size } => (*block_size, 1u32),
4699 QuantScheme::Int4Block { block_size } => (*block_size, 2u32),
4700 QuantScheme::Fp8E4m3 => (1, 3u32),
4701 QuantScheme::Fp8E5m2 => (1, 4u32),
4702 QuantScheme::Nvfp4Block => (rlx_ir::NVFP4_GROUP_SIZE as u32, 5u32),
4703 other => panic!("rlx-wgpu DequantMatMul: unsupported scheme {other:?}"),
4704 };
4705 let scale_id = node.inputs[2];
4706 let zp_id = node.inputs[3];
4707 let p = DequantMatmulParams {
4708 m,
4709 k,
4710 n,
4711 block_size,
4712 scheme_id,
4713 x_off: (arena.offset(x_id) / 4) as u32,
4714 w_off: (arena.offset(w_id) / 4) as u32,
4715 scale_off: (arena.offset(scale_id) / 4) as u32,
4716 zp_off: (arena.offset(zp_id) / 4) as u32,
4717 out_off: (arena.offset(node.id) / 4) as u32,
4718 _p0: 0,
4719 _p1: 0,
4720 };
4721 schedule.push(Step::DequantMatmul { params: p });
4722 let dk = dequant_matmul_kernel(&dev.device);
4723 let u = emit_uniform(std::mem::size_of::<DequantMatmulParams>());
4724 let bg = bind_op_output_window(&dev.device, dk, &arena, node.id, &u);
4725 uniforms.push(u);
4726 bind_groups.push(bg);
4727 }
4728 }
4729 Op::RmsNormBackwardInput { eps, .. }
4730 | Op::RmsNormBackwardGamma { eps, .. }
4731 | Op::RmsNormBackwardBeta { eps, .. } => {
4732 let x_shape = &graph.node(node.inputs[0]).shape;
4733 let h = x_shape.dim(x_shape.rank() - 1).unwrap_static() as u32;
4734 let rows = (x_shape.num_elements().unwrap() / h.max(1) as usize) as u32;
4735 let foff = |i: usize| (arena.offset(node.inputs[i]) / 4) as u32;
4736 let wrt = match &node.op {
4737 Op::RmsNormBackwardInput { .. } => 0u32,
4738 Op::RmsNormBackwardGamma { .. } => 1u32,
4739 Op::RmsNormBackwardBeta { .. } => 2u32,
4740 _ => unreachable!(),
4741 };
4742 let p = RmsNormBwdParams {
4743 outer: rows,
4744 inner: h,
4745 x_off: foff(0),
4746 gamma_off: foff(1),
4747 beta_off: foff(2),
4748 dy_off: foff(3),
4749 out_off: (arena.offset(node.id) / 4) as u32,
4750 eps_bits: eps.to_bits(),
4751 wrt,
4752 };
4753 let rk = if wrt == 0 {
4754 rms_norm_backward_kernel(&dev.device)
4755 } else {
4756 rms_norm_backward_param_kernel(&dev.device)
4757 };
4758 let u = emit_uniform(std::mem::size_of::<RmsNormBwdParams>());
4759 let bg = bind_op_output_window(&dev.device, rk, &arena, node.id, &u);
4760 match &node.op {
4761 Op::RmsNormBackwardInput { .. } => {
4762 schedule.push(Step::RmsNormBackwardInput { params: p });
4763 }
4764 Op::RmsNormBackwardGamma { .. } => {
4765 schedule.push(Step::RmsNormBackwardGamma { params: p });
4766 }
4767 Op::RmsNormBackwardBeta { .. } => {
4768 schedule.push(Step::RmsNormBackwardBeta { params: p });
4769 }
4770 _ => unreachable!(),
4771 }
4772 uniforms.push(u);
4773 bind_groups.push(bg);
4774 }
4775 Op::LayerNormBackwardInput { eps, .. } => {
4776 let x_shape = &graph.node(node.inputs[0]).shape;
4777 let h = x_shape.dim(x_shape.rank() - 1).unwrap_static() as u32;
4778 let rows = (x_shape.num_elements().unwrap() / h.max(1) as usize) as u32;
4779 let p = LayerNormBwdParams {
4780 outer: rows,
4781 inner: h,
4782 x_off: (arena.offset(node.inputs[0]) / 4) as u32,
4783 gamma_off: (arena.offset(node.inputs[1]) / 4) as u32,
4784 dy_off: (arena.offset(node.inputs[2]) / 4) as u32,
4785 out_off: (arena.offset(node.id) / 4) as u32,
4786 eps_bits: eps.to_bits(),
4787 scratch_off: 0,
4788 };
4789 let rk = layer_norm_backward_input_kernel(&dev.device);
4790 let u = emit_uniform(std::mem::size_of::<LayerNormBwdParams>());
4791 let bg = bind_op_output_window(&dev.device, rk, &arena, node.id, &u);
4792 schedule.push(Step::LayerNormBackwardInput { params: p });
4793 uniforms.push(u);
4794 bind_groups.push(bg);
4795 }
4796 Op::LayerNormBackwardGamma { eps, .. } => {
4797 let x_shape = &graph.node(node.inputs[0]).shape;
4803 let h = x_shape.dim(x_shape.rank() - 1).unwrap_static() as u32;
4804 let rows = (x_shape.num_elements().unwrap() / h.max(1) as usize) as u32;
4805 const ROWS_PER_WG: u32 = 16;
4806 let num_workgroups = rows.div_ceil(ROWS_PER_WG.max(1));
4807 let scratch_off_words = (arena.scratch_off / 4) as u32;
4808 let partial_params = LayerNormBwdParams {
4809 outer: rows,
4810 inner: h,
4811 x_off: (arena.offset(node.inputs[0]) / 4) as u32,
4812 gamma_off: 0,
4813 dy_off: (arena.offset(node.inputs[1]) / 4) as u32,
4814 out_off: 0, eps_bits: eps.to_bits(),
4816 scratch_off: scratch_off_words,
4817 };
4818 let reduce_params = LayerNormBwdParams {
4819 outer: num_workgroups,
4822 inner: h,
4823 x_off: 0,
4824 gamma_off: 0,
4825 dy_off: 0,
4826 out_off: (arena.offset(node.id) / 4) as u32,
4827 eps_bits: eps.to_bits(),
4828 scratch_off: scratch_off_words,
4829 };
4830 let p_k = layer_norm_backward_gamma_partial_kernel(&dev.device);
4831 let r_k = layer_norm_backward_gamma_reduce_kernel(&dev.device);
4832 let p_u = emit_uniform(std::mem::size_of::<LayerNormBwdParams>());
4833 let r_u = emit_uniform(std::mem::size_of::<LayerNormBwdParams>());
4834 let p_bg = bind_op_output_window(&dev.device, p_k, &arena, node.id, &p_u);
4835 let r_bg = bind_op_output_window(&dev.device, r_k, &arena, node.id, &r_u);
4836 schedule.push(Step::LayerNormBackwardGammaPartial {
4837 params: partial_params,
4838 num_workgroups,
4839 });
4840 schedule.push(Step::LayerNormBackwardGammaReduce {
4841 params: reduce_params,
4842 });
4843 uniforms.push(p_u);
4844 uniforms.push(r_u);
4845 bind_groups.push(p_bg);
4846 bind_groups.push(r_bg);
4847 }
4848 Op::RopeBackward { head_dim, n_rot } => {
4849 let dy_shape = &graph.node(node.inputs[0]).shape;
4850 let (batch, seq, hidden) = if dy_shape.rank() >= 3 {
4851 (
4852 dy_shape.dim(0).unwrap_static() as u32,
4853 dy_shape.dim(1).unwrap_static() as u32,
4854 dy_shape.dim(2).unwrap_static() as u32,
4855 )
4856 } else {
4857 (
4858 1,
4859 dy_shape.dim(0).unwrap_static() as u32,
4860 dy_shape.dim(1).unwrap_static() as u32,
4861 )
4862 };
4863 let cos_len = graph.node(node.inputs[1]).shape.num_elements().unwrap() as u32;
4864 let p = RopeBwdParams {
4865 batch,
4866 seq,
4867 hidden,
4868 head_dim: *head_dim as u32,
4869 n_rot: *n_rot as u32,
4870 dy_off: (arena.offset(node.inputs[0]) / 4) as u32,
4871 cos_off: (arena.offset(node.inputs[1]) / 4) as u32,
4872 sin_off: (arena.offset(node.inputs[2]) / 4) as u32,
4873 dx_off: (arena.offset(node.id) / 4) as u32,
4874 cos_len,
4875 };
4876 let rk = rope_backward_kernel(&dev.device);
4877 let u = emit_uniform(std::mem::size_of::<RopeBwdParams>());
4878 let bg = bind_op_output_window(&dev.device, rk, &arena, node.id, &u);
4879 schedule.push(Step::RopeBackward { params: p });
4880 uniforms.push(u);
4881 bind_groups.push(bg);
4882 }
4883 Op::CumsumBackward { exclusive, .. } => {
4884 let dy_shape = &graph.node(node.inputs[0]).shape;
4885 let cols = dy_shape.dim(dy_shape.rank() - 1).unwrap_static() as u32;
4886 let rows = (dy_shape.num_elements().unwrap() / cols.max(1) as usize) as u32;
4887 let p = CumsumBwdParams {
4888 outer: rows,
4889 inner: cols,
4890 dy_off: (arena.offset(node.inputs[0]) / 4) as u32,
4891 dx_off: (arena.offset(node.id) / 4) as u32,
4892 exclusive: if *exclusive { 1 } else { 0 },
4893 _p0: 0,
4894 _p1: 0,
4895 _p2: 0,
4896 };
4897 let ck = cumsum_backward_kernel(&dev.device);
4898 let u = emit_uniform(std::mem::size_of::<CumsumBwdParams>());
4899 let bg = bind_op_output_window(&dev.device, ck, &arena, node.id, &u);
4900 schedule.push(Step::CumsumBackward { params: p });
4901 uniforms.push(u);
4902 bind_groups.push(bg);
4903 }
4904 Op::GatherBackward { .. } => {
4905 let dy_shape = &graph.node(node.inputs[0]).shape;
4906 let idx_shape = &graph.node(node.inputs[1]).shape;
4907 let out_shape = &node.shape;
4908 let rank = out_shape.rank();
4909 let axis = match &node.op {
4910 Op::GatherBackward { axis } => *axis,
4911 _ => 0,
4912 };
4913 let axis_u = if axis < 0 {
4914 (rank as i32 + axis) as usize
4915 } else {
4916 axis as usize
4917 };
4918 let outer: usize = (0..axis_u)
4919 .map(|i| dy_shape.dim(i).unwrap_static())
4920 .product::<usize>()
4921 .max(1);
4922 let num_idx = idx_shape.dim(axis_u).unwrap_static();
4923 let trailing: usize = (axis_u + 1..dy_shape.rank())
4924 .map(|i| dy_shape.dim(i).unwrap_static())
4925 .product::<usize>()
4926 .max(1);
4927 let axis_dim = out_shape.dim(axis_u).unwrap_static();
4928 let p = GatherBwdParams {
4929 outer: outer as u32,
4930 axis_dim: axis_dim as u32,
4931 num_idx: num_idx as u32,
4932 trailing: trailing as u32,
4933 dy_off: (arena.offset(node.inputs[0]) / 4) as u32,
4934 idx_off: (arena.offset(node.inputs[1]) / 4) as u32,
4935 dst_off: (arena.offset(node.id) / 4) as u32,
4936 _p0: 0,
4937 };
4938 let zk = gather_backward_zero_kernel(&dev.device);
4939 let u = emit_uniform(std::mem::size_of::<GatherBwdParams>());
4940 let bg = bind_op_output_window(&dev.device, zk, &arena, node.id, &u);
4941 schedule.push(Step::GatherBackward { params: p });
4942 uniforms.push(u);
4943 bind_groups.push(bg);
4944 }
4945 #[cfg(feature = "splat")]
4946 Op::GaussianSplatRender {
4947 width,
4948 height,
4949 tile_size,
4950 radius_scale,
4951 alpha_cutoff,
4952 max_splat_steps,
4953 transmittance_threshold,
4954 max_list_entries,
4955 } => {
4956 let elem_len = |id: NodeId| -> u32 {
4957 graph.node(id).shape.num_elements().unwrap_or(0) as u32
4958 };
4959 schedule.push(Step::GaussianSplatRender {
4960 positions_byte_off: arena.offset(node.inputs[0]) as u32,
4961 positions_len: elem_len(node.inputs[0]),
4962 scales_byte_off: arena.offset(node.inputs[1]) as u32,
4963 scales_len: elem_len(node.inputs[1]),
4964 rotations_byte_off: arena.offset(node.inputs[2]) as u32,
4965 rotations_len: elem_len(node.inputs[2]),
4966 opacities_byte_off: arena.offset(node.inputs[3]) as u32,
4967 opacities_len: elem_len(node.inputs[3]),
4968 colors_byte_off: arena.offset(node.inputs[4]) as u32,
4969 colors_len: elem_len(node.inputs[4]),
4970 sh_coeffs_byte_off: arena.offset(node.inputs[5]) as u32,
4971 sh_coeffs_len: elem_len(node.inputs[5]),
4972 meta_byte_off: arena.offset(node.inputs[6]) as u32,
4973 dst_byte_off: arena.offset(node.id) as u32,
4974 dst_len: node.shape.num_elements().unwrap_or(0) as u32,
4975 width: *width,
4976 height: *height,
4977 tile_size: *tile_size,
4978 radius_scale: *radius_scale,
4979 alpha_cutoff: *alpha_cutoff,
4980 max_splat_steps: *max_splat_steps,
4981 transmittance_threshold: *transmittance_threshold,
4982 max_list_entries: *max_list_entries,
4983 });
4984 }
4985
4986 #[cfg(feature = "splat")]
4987 Op::GaussianSplatRenderBackward {
4988 width,
4989 height,
4990 tile_size,
4991 radius_scale,
4992 alpha_cutoff,
4993 max_splat_steps,
4994 transmittance_threshold,
4995 max_list_entries,
4996 loss_grad_clip,
4997 sh_band,
4998 max_anisotropy,
4999 } => {
5000 let elem_len = |id: NodeId| -> u32 {
5001 graph.node(id).shape.num_elements().unwrap_or(0) as u32
5002 };
5003 schedule.push(Step::GaussianSplatRenderBackward {
5004 positions_byte_off: arena.offset(node.inputs[0]) as u32,
5005 positions_len: elem_len(node.inputs[0]),
5006 scales_byte_off: arena.offset(node.inputs[1]) as u32,
5007 scales_len: elem_len(node.inputs[1]),
5008 rotations_byte_off: arena.offset(node.inputs[2]) as u32,
5009 rotations_len: elem_len(node.inputs[2]),
5010 opacities_byte_off: arena.offset(node.inputs[3]) as u32,
5011 opacities_len: elem_len(node.inputs[3]),
5012 colors_byte_off: arena.offset(node.inputs[4]) as u32,
5013 colors_len: elem_len(node.inputs[4]),
5014 sh_coeffs_byte_off: arena.offset(node.inputs[5]) as u32,
5015 sh_coeffs_len: elem_len(node.inputs[5]),
5016 meta_byte_off: arena.offset(node.inputs[6]) as u32,
5017 d_loss_byte_off: arena.offset(node.inputs[7]) as u32,
5018 d_loss_len: elem_len(node.inputs[7]),
5019 packed_byte_off: arena.offset(node.id) as u32,
5020 packed_len: node.shape.num_elements().unwrap_or(0) as u32,
5021 width: *width,
5022 height: *height,
5023 tile_size: *tile_size,
5024 radius_scale: *radius_scale,
5025 alpha_cutoff: *alpha_cutoff,
5026 max_splat_steps: *max_splat_steps,
5027 transmittance_threshold: *transmittance_threshold,
5028 max_list_entries: *max_list_entries,
5029 loss_grad_clip: *loss_grad_clip,
5030 sh_band: *sh_band,
5031 max_anisotropy: *max_anisotropy,
5032 });
5033 }
5034
5035 #[cfg(feature = "splat")]
5036 Op::GaussianSplatPrepare {
5037 width,
5038 height,
5039 tile_size,
5040 radius_scale,
5041 alpha_cutoff,
5042 max_splat_steps,
5043 transmittance_threshold,
5044 max_list_entries,
5045 } => {
5046 let elem_len = |id: NodeId| -> u32 {
5047 graph.node(id).shape.num_elements().unwrap_or(0) as u32
5048 };
5049 schedule.push(Step::GaussianSplatPrepare {
5050 positions_byte_off: arena.offset(node.inputs[0]) as u32,
5051 positions_len: elem_len(node.inputs[0]),
5052 scales_byte_off: arena.offset(node.inputs[1]) as u32,
5053 scales_len: elem_len(node.inputs[1]),
5054 rotations_byte_off: arena.offset(node.inputs[2]) as u32,
5055 rotations_len: elem_len(node.inputs[2]),
5056 opacities_byte_off: arena.offset(node.inputs[3]) as u32,
5057 opacities_len: elem_len(node.inputs[3]),
5058 colors_byte_off: arena.offset(node.inputs[4]) as u32,
5059 colors_len: elem_len(node.inputs[4]),
5060 sh_coeffs_byte_off: arena.offset(node.inputs[5]) as u32,
5061 sh_coeffs_len: elem_len(node.inputs[5]),
5062 meta_byte_off: arena.offset(node.inputs[6]) as u32,
5063 meta_len: elem_len(node.inputs[6]),
5064 prep_byte_off: arena.offset(node.id) as u32,
5065 prep_len: node.shape.num_elements().unwrap_or(0) as u32,
5066 width: *width,
5067 height: *height,
5068 tile_size: *tile_size,
5069 radius_scale: *radius_scale,
5070 alpha_cutoff: *alpha_cutoff,
5071 max_splat_steps: *max_splat_steps,
5072 transmittance_threshold: *transmittance_threshold,
5073 max_list_entries: *max_list_entries,
5074 });
5075 }
5076
5077 #[cfg(feature = "splat")]
5078 Op::GaussianSplatRasterize {
5079 width,
5080 height,
5081 tile_size,
5082 alpha_cutoff,
5083 max_splat_steps,
5084 transmittance_threshold,
5085 max_list_entries,
5086 } => {
5087 let elem_len = |id: NodeId| -> u32 {
5088 graph.node(id).shape.num_elements().unwrap_or(0) as u32
5089 };
5090 let prep_id = node.inputs[0];
5091 let count = match &graph.node(prep_id).op {
5092 rlx_ir::Op::GaussianSplatPrepare { .. } => {
5093 elem_len(graph.node(prep_id).inputs[0]) / 3
5094 }
5095 _ => 1,
5096 };
5097 schedule.push(Step::GaussianSplatRasterize {
5098 prep_byte_off: arena.offset(prep_id) as u32,
5099 prep_len: elem_len(prep_id),
5100 meta_byte_off: arena.offset(node.inputs[1]) as u32,
5101 meta_len: elem_len(node.inputs[1]),
5102 dst_byte_off: arena.offset(node.id) as u32,
5103 dst_len: node.shape.num_elements().unwrap_or(0) as u32,
5104 count,
5105 width: *width,
5106 height: *height,
5107 tile_size: *tile_size,
5108 alpha_cutoff: *alpha_cutoff,
5109 max_splat_steps: *max_splat_steps,
5110 transmittance_threshold: *transmittance_threshold,
5111 max_list_entries: *max_list_entries,
5112 });
5113 }
5114
5115 Op::If { .. } | Op::While { .. } => {
5116 panic!(
5121 "rlx-wgpu: Op::If/While leaked past unfusion pass — \
5122 check unfuse.rs::expand_if / expand_while"
5123 );
5124 }
5125 Op::RngNormal {
5126 mean,
5127 scale,
5128 key,
5129 op_seed,
5130 } => {
5131 let len = node.shape.num_elements().unwrap_or(0) as u32;
5132 schedule.push(Step::RngNormalHost {
5133 dst_byte_off: arena.offset(node.id) as u32,
5134 len,
5135 mean: *mean,
5136 scale: *scale,
5137 key: *key,
5138 op_seed: *op_seed,
5139 });
5140 }
5141 Op::RngUniform {
5142 low,
5143 high,
5144 key,
5145 op_seed,
5146 } => {
5147 let len = node.shape.num_elements().unwrap_or(0) as u32;
5148 schedule.push(Step::RngUniformHost {
5149 dst_byte_off: arena.offset(node.id) as u32,
5150 len,
5151 low: *low,
5152 high: *high,
5153 key: *key,
5154 op_seed: *op_seed,
5155 });
5156 }
5157 Op::TransformRegion { steps, .. }
5161 if steps.len() == 1
5162 && matches!(
5163 steps[0],
5164 rlx_ir::op::TransformStep::ResizeNearest2x(
5165 rlx_ir::op::ChainOperand::Input(0)
5166 )
5167 ) =>
5168 {
5169 let in_shape = &graph.node(node.inputs[0]).shape;
5170 schedule.push(Step::ResizeNearest2xHost {
5171 src_byte_off: arena.offset(node.inputs[0]) as u32,
5172 dst_byte_off: arena.offset(node.id) as u32,
5173 n: in_shape.dim(0).unwrap_static() as u32,
5174 c: in_shape.dim(1).unwrap_static() as u32,
5175 h: in_shape.dim(2).unwrap_static() as u32,
5176 w: in_shape.dim(3).unwrap_static() as u32,
5177 });
5178 }
5179 other => panic!(
5180 "rlx-wgpu: op {other:?} not yet lowered (v2 covers Matmul, \
5181 Binary, Compare, Activation, Where — fall back to CPU/Metal/MLX)"
5182 ),
5183 }
5184 }
5185
5186 if rlx_ir::env::flag("RLX_WGPU_SCHEDULE") || rlx_ir::env::flag("RLX_DISPATCH_REPORT") {
5187 let mut counts: std::collections::BTreeMap<&'static str, usize> =
5188 std::collections::BTreeMap::new();
5189 let mut fft_gpu = 0usize;
5190 let mut fft_host = 0usize;
5191 for s in &schedule {
5192 *counts.entry(step_name(s)).or_insert(0) += 1;
5193 match s {
5194 Step::FftGpu { .. } => fft_gpu += 1,
5195 Step::FftHost { .. } => fft_host += 1,
5196 _ => {}
5197 }
5198 }
5199 let arena_mb = arena.size as f64 / (1u64 << 20) as f64;
5200 eprintln!(
5201 "[rlx-wgpu] schedule: {} steps, arena={arena_mb:.1} MiB, fft_gpu={fft_gpu}, fft_host={fft_host}",
5202 schedule.len()
5203 );
5204 for (n, c) in &counts {
5205 eprintln!(" {c:>4} × {n}");
5206 }
5207 }
5208
5209 let coop_f16_vk = schedule_uses_coop_f16_vk(&schedule);
5210
5211 Self {
5212 graph,
5213 arena,
5214 dequant_scratch_off,
5215 schedule,
5216 input_offsets,
5217 param_offsets,
5218 uniforms,
5219 bind_groups,
5220 meta_buffers,
5221 unresolved: None,
5222 last_binding: None,
5223 pending_params: HashMap::new(),
5224 pending_param_bytes: HashMap::new(),
5225 active_extent: None,
5226 uniforms_active_extent: None,
5227 input_staging_hashes: HashMap::new(),
5228 coop_f16_vk,
5229 coop_f16_b_param,
5230 coop_f16_vk_wide_b: HashSet::new(),
5231 coop_f16_vk_wide_bind_groups,
5232 coop_f16_host_activations,
5233 stashed_params: HashMap::new(),
5234 readback_staging: None,
5235 tiny_readback: None,
5236 dispatch_only: false,
5237 fft_gpu_steps,
5238 gpu_handles: HashMap::new(),
5239 gpu_handle_feeds: HashMap::new(),
5240 gpu_handle_resident: HashSet::new(),
5241 pending_read_indices: None,
5242 rng,
5243 }
5244 }
5245}