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