Skip to main content

rlx_wgpu/backend/
run.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, version 3.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! `run` — extracted from the `backend` module for navigability (see `mod.rs`).
17
18#![allow(unused_imports)]
19
20use crate::buffer::{
21    Arena, ReadbackLayout, ReadbackStaging, TinyReadbackStaging, decode_mapped_readback_f32,
22    decode_tiny_mapped_f32, encode_readback_copies, plan_f32_uniform, read_f32_many_pooled,
23    schedule_readback_map, use_tiny_readback, wait_readback_map,
24};
25use crate::device::wgpu_device;
26use crate::kernels::{
27    ArgmaxParams, AttentionBwdParams, AttentionParams, BatchElementwiseRegionParams, BinaryParams,
28    Conv1dParams, Conv2dParams, Conv3dParams, CopyParams, CumsumBwdParams, CumsumParams,
29    DequantMatmulParams, ElementwiseRegionParams, ExpandParams, FmaParams, FusedResidualLnParams,
30    FusedResidualLnTeeParams, FusedResidualRmsNormParams, GatherAxisParams, GatherBwdParams,
31    GatherParams, GroupedMatmulParams, GruParams, Kernel, LayerNormBwdParams, LayerNormParams,
32    Mamba2Params, MatmulParams, MatmulQkvParams, NarrowConcatParams, Pool1dParams, Pool2dParams,
33    Pool3dParams, ReduceParams, RmsNormBwdParams, RnnParams, RopeBwdParams, RopeParams,
34    SampleParams, ScatterAddParams, SceParams, SelectiveScanParams, SoftmaxParams, TopKParams,
35    TransposeParams, UmapKnnParams, UnaryParams, WelchPeaksGpuParams, WhereParams, argmax_kernel,
36    attention_bwd_kernel, attention_kernel, batch_elementwise_region_kernel, binary_kernel,
37    cast_f32_to_f16_kernel, compare_kernel, concat_kernel, conv1d_kernel, conv1d_tiled_kernel,
38    conv2d_kernel, conv3d_kernel, copy_kernel, cumsum_backward_kernel, cumsum_kernel,
39    dequant_matmul_kernel, elementwise_region_kernel, elementwise_region_spatial_kernel,
40    expand_kernel, fma_kernel, fused_residual_ln_kernel, fused_residual_ln_tee_kernel,
41    fused_residual_rms_norm_kernel, gather_axis_kernel, gather_backward_acc_kernel,
42    gather_backward_zero_kernel, gather_kernel, gather_split_kernel, grouped_matmul_kernel,
43    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 run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
67        self.run_read_outputs(inputs, None)
68    }
69
70    pub fn run_read_outputs(
71        &mut self,
72        inputs: &[(&str, &[f32])],
73        read_indices: Option<&[usize]>,
74    ) -> Vec<Vec<f32>> {
75        self.pending_read_indices = read_indices.map(|s| s.to_vec());
76        let outs = self.run_inner(inputs);
77        self.pending_read_indices = None;
78        outs
79    }
80
81    /// Async sibling of [`Self::run`] for the browser, where GPU→CPU readback
82    /// cannot block the event loop. All compute is dispatched + submitted
83    /// synchronously (via the normal `run_inner` in dispatch-only mode); the
84    /// outputs are then read back from the arena asynchronously.
85    ///
86    /// Supports pure feed-forward graphs only — graphs with host-executed ops
87    /// (GGUF dequant, LSTM/GRU, GatedDeltaNet, FFT-host, …) map intermediate
88    /// results back mid-graph, which would block the browser. Such graphs
89    /// panic with a clear message rather than hang.
90    #[cfg(target_arch = "wasm32")]
91    pub async fn run_async(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
92        assert!(
93            !self
94                .schedule
95                .iter()
96                .any(|s| step_runs_on_host(s) || step_is_tail_host(s)),
97            "rlx-wgpu run_async: graph contains host-executed ops unsupported on wasm"
98        );
99
100        // 1. Dispatch + submit all compute, skipping the blocking readback.
101        self.dispatch_only = true;
102        let _ = self.run_inner(inputs);
103        self.dispatch_only = false;
104
105        let dev =
106            wgpu_device().expect("rlx-wgpu: device not initialized (call init_wgpu_device first)");
107
108        // 2. Read the selected outputs back from the arena asynchronously.
109        let plan = self.readback_plan();
110        let out_ids_all: Vec<_> = self.graph.outputs.clone();
111        let out_ids: Vec<_> = plan.iter().map(|&i| out_ids_all[i]).collect();
112        let layout = ReadbackLayout::for_nodes(&self.arena, &out_ids);
113        if layout.regions.is_empty() {
114            return Vec::new();
115        }
116        ReadbackStaging::prepare(&dev.device, &mut self.readback_staging, layout.total_bytes);
117        let staging_buf = self
118            .readback_staging
119            .as_ref()
120            .expect("readback staging")
121            .buffer()
122            .clone();
123
124        let mut enc = dev
125            .device
126            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
127                label: Some("rlx-wgpu async readback"),
128            });
129        encode_readback_copies(&mut enc, &self.arena, &staging_buf, &out_ids, &layout);
130        dev.queue.submit(std::iter::once(enc.finish()));
131
132        crate::buffer::wasm_async::map_read_async(&staging_buf, layout.total_bytes)
133            .await
134            .expect("rlx-wgpu async buffer map failed");
135
136        let partial = decode_mapped_readback_f32(&staging_buf, &layout);
137        self.pack_readback_outputs(&plan, partial)
138    }
139
140    pub(crate) fn run_tail_host_audio_ops(&self, dev: &crate::device::WgpuDevice) {
141        if !self.schedule.iter().any(step_is_tail_host) {
142            return;
143        }
144        for step in &self.schedule {
145            if !step_is_tail_host(step) {
146                continue;
147            }
148            match step {
149                Step::WelchPeaksHost {
150                    spec_byte_off,
151                    dst_byte_off,
152                    welch_batch,
153                    n_fft,
154                    n_segments,
155                    k,
156                } => {
157                    crate::welch_peaks_host::run_welch_peaks(
158                        &self.arena,
159                        &dev.device,
160                        &dev.queue,
161                        *spec_byte_off as usize,
162                        *dst_byte_off as usize,
163                        *welch_batch as usize,
164                        *n_fft as usize,
165                        *n_segments as usize,
166                        *k as usize,
167                    );
168                }
169                Step::LogMelHost {
170                    spec_byte_off,
171                    filt_byte_off,
172                    dst_byte_off,
173                    outer,
174                    n_fft,
175                    n_bins,
176                    n_mels,
177                } => {
178                    crate::log_mel_host::run_log_mel(
179                        &self.arena,
180                        &dev.device,
181                        &dev.queue,
182                        *spec_byte_off as usize,
183                        *filt_byte_off as usize,
184                        *dst_byte_off as usize,
185                        *outer as usize,
186                        *n_fft as usize,
187                        *n_bins as usize,
188                        *n_mels as usize,
189                    );
190                }
191                Step::LogMelBackwardHost {
192                    spec_byte_off,
193                    filt_byte_off,
194                    dy_byte_off,
195                    dst_byte_off,
196                    outer,
197                    n_fft,
198                    n_bins,
199                    n_mels,
200                } => {
201                    crate::log_mel_host::run_log_mel_backward(
202                        &self.arena,
203                        &dev.device,
204                        &dev.queue,
205                        *spec_byte_off as usize,
206                        *filt_byte_off as usize,
207                        *dy_byte_off as usize,
208                        *dst_byte_off as usize,
209                        *outer as usize,
210                        *n_fft as usize,
211                        *n_bins as usize,
212                        *n_mels as usize,
213                    );
214                }
215                _ => {}
216            }
217        }
218    }
219
220    pub(crate) fn run_inner(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
221        // Lazy compile path: if we deferred compile waiting for shapes,
222        // infer the binding from input data lengths now and compile.
223        if self.unresolved.is_some() {
224            self.lazy_compile_for_inputs(inputs);
225        }
226        let dev = wgpu_device().expect("rlx-wgpu: device gone");
227        self.stage_gpu_handle_inputs(dev, inputs);
228        let skip_input_upload =
229            !rlx_ir::env::flag("RLX_WGPU_FORCE_INPUT_UPLOAD") && !self.coop_f16_vk;
230        for &(name, data) in inputs {
231            if let Some(&id) = self.input_offsets.get(name)
232                && self.arena.has(id)
233            {
234                if skip_input_upload {
235                    let h = hash_f32_input(data);
236                    if self.input_staging_hashes.get(name) == Some(&h) {
237                        if self.arena.f16_buffer.is_some() {
238                            self.arena.write_f16_shadow(&dev.queue, id, data);
239                        }
240                        continue;
241                    }
242                    self.arena.write_f32(&dev.queue, id, data);
243                    self.input_staging_hashes.insert(name.to_string(), h);
244                } else {
245                    self.arena.write_f32(&dev.queue, id, data);
246                }
247            }
248        }
249        for &(act_id, act, ref src_name) in &self.coop_f16_host_activations {
250            let src =
251                host_tensor_f32(src_name, inputs, &self.stashed_params).unwrap_or_else(|| {
252                    panic!("rlx-wgpu CoopF16Vk host activation: missing tensor {src_name:?}")
253                });
254            let mirrored = apply_activation_host(act, src);
255            self.arena.write_f32(&dev.queue, act_id, &mirrored);
256        }
257        if !self.coop_f16_host_activations.is_empty() {
258            // Ensure host staging writes are visible before CoopF16Vk reads f16.
259            let flush = dev
260                .device
261                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
262                    label: Some("rlx-wgpu host mirror flush"),
263                });
264            dev.queue.submit(std::iter::once(flush.finish()));
265            let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
266        }
267
268        // Active-extent (PLAN L1): scale safe Steps' primary dim by
269        // actual/upper. Used in BOTH the uniform-write loop (so the
270        // kernel sees the scaled count) AND the dispatch loop (so the
271        // workgroup grid is shrunk).
272        let active = self.active_extent.filter(|_| self.all_safe_for_active());
273        let scale = |full: u32| -> u32 {
274            match active {
275                Some((a, u)) if u > 0 => {
276                    let f = full as usize;
277                    (f * a).div_ceil(u).min(f) as u32
278                }
279                _ => full,
280            }
281        };
282
283        // Stage uniform writes — but skip the loop entirely when the
284        // bytes already in the uniforms match this run's active extent.
285        // BERT inference at fixed batch hits this path: 100+ tiny
286        // queue.write_buffer calls (one per Step) collapse to zero,
287        // saving milliseconds of staging-copy overhead.
288        let need_uniform_writes = self.uniforms_active_extent != Some(active);
289        if need_uniform_writes {
290            let mut gpu_ui = 0usize;
291            for step in self.schedule.iter() {
292                if step_runs_on_host(step) {
293                    continue;
294                }
295                match step {
296                    Step::CastF32ToF16 { .. } => {
297                        // Params are static for this step (offset+len), so the
298                        // pre-pass write at compile time is sufficient. No
299                        // active-extent scaling — len is the full element count.
300                    }
301                    Step::Matmul {
302                        m,
303                        k,
304                        n,
305                        a_off_f32,
306                        b_off_f32,
307                        c_off_f32,
308                        batch,
309                        a_batch_stride,
310                        b_batch_stride,
311                        c_batch_stride,
312                        has_bias,
313                        bias_off_f32,
314                        act_id,
315                        b_is_param: _,
316                        compute_precision: _,
317                    } => {
318                        // PLAN L1 (safe at any batch — c_batch_stride is
319                        // pre-baked at compile time at FULL m, so scaling
320                        // params.m only changes per-thread bound checks).
321                        let m_scaled = scale(*m);
322                        let p = MatmulParams {
323                            m: m_scaled,
324                            k: *k,
325                            n: *n,
326                            a_off: *a_off_f32,
327                            b_off: *b_off_f32,
328                            c_off: *c_off_f32,
329                            batch: *batch,
330                            a_batch_stride: *a_batch_stride,
331                            b_batch_stride: *b_batch_stride,
332                            c_batch_stride: *c_batch_stride,
333                            has_bias: *has_bias,
334                            bias_off: *bias_off_f32,
335                            act_id: *act_id,
336                            _pad0: 0,
337                            _pad1: 0,
338                            _pad2: 0,
339                        };
340                        dev.queue
341                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
342                    }
343                    Step::Binary { params } | Step::Compare { params } => {
344                        let mut p = *params;
345                        p.n = scale(p.n);
346                        dev.queue
347                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
348                    }
349                    Step::Unary { params, .. } => {
350                        let mut p = *params;
351                        p.n = scale(p.n);
352                        dev.queue
353                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
354                    }
355                    Step::Where { params } => {
356                        let mut p = *params;
357                        p.n = scale(p.n);
358                        dev.queue
359                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
360                    }
361                    Step::Fma { params } => {
362                        let mut p = *params;
363                        p.n = scale(p.n);
364                        dev.queue
365                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
366                    }
367                    Step::Reduce { params } => {
368                        let mut p = *params;
369                        p.outer = scale(p.outer);
370                        dev.queue
371                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
372                    }
373                    Step::Softmax { params } => {
374                        let mut p = *params;
375                        p.outer = scale(p.outer);
376                        dev.queue
377                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
378                    }
379                    Step::SoftmaxCrossEntropy { params } => {
380                        let mut p = *params;
381                        p.outer = scale(p.outer);
382                        dev.queue
383                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
384                    }
385                    Step::LayerNorm { params } => {
386                        let mut p = *params;
387                        p.outer = scale(p.outer);
388                        dev.queue
389                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
390                    }
391                    Step::RmsNormBackwardInput { params }
392                    | Step::RmsNormBackwardGamma { params }
393                    | Step::RmsNormBackwardBeta { params } => {
394                        let mut p = *params;
395                        p.outer = scale(p.outer);
396                        dev.queue
397                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
398                    }
399                    Step::LayerNormBackwardInput { params } => {
400                        let mut p = *params;
401                        p.outer = scale(p.outer);
402                        dev.queue
403                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
404                    }
405                    Step::LayerNormBackwardGammaPartial { params, .. } => {
406                        let mut p = *params;
407                        p.outer = scale(p.outer);
408                        dev.queue
409                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
410                    }
411                    Step::LayerNormBackwardGammaReduce { params } => {
412                        // `outer` here is the partial chunk count (not
413                        // a batch dim) — do NOT apply active-extent
414                        // scaling.
415                        dev.queue.write_buffer(
416                            &self.uniforms[gpu_ui],
417                            0,
418                            bytemuck::bytes_of(params),
419                        );
420                    }
421                    Step::CumsumBackward { params } => {
422                        let mut p = *params;
423                        p.outer = scale(p.outer);
424                        dev.queue
425                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
426                    }
427                    Step::RopeBackward { params } => {
428                        let mut p = *params;
429                        p.seq = scale(p.seq);
430                        dev.queue
431                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
432                    }
433                    Step::GatherBackward { params } => {
434                        let mut p = *params;
435                        p.outer = scale(p.outer);
436                        dev.queue
437                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
438                    }
439                    Step::Cumsum { params } => {
440                        let mut p = *params;
441                        p.outer = scale(p.outer);
442                        dev.queue
443                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
444                    }
445                    Step::FftGpu { .. } => {}
446                    Step::Copy { params } => {
447                        let mut p = *params;
448                        p.n = scale(p.n);
449                        dev.queue
450                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
451                    }
452                    Step::BufferCopy { .. } => {}
453                    Step::ElementwiseRegion { params } => {
454                        // Active-extent: scale element count.
455                        let mut p = *params;
456                        p.len = scale(p.len);
457                        dev.queue
458                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
459                    }
460                    Step::BatchElementwiseRegion { params } => {
461                        let mut p = *params;
462                        p.slice_len = scale(p.slice_len);
463                        p.num_batch = scale(p.num_batch);
464                        dev.queue
465                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
466                    }
467                    Step::Transpose { params, .. } => {
468                        // PLAN L1: when bucket_outermost == 1, scale
469                        // `out_total` proportional to scaling `out_dim_0`.
470                        // Other transposes leave out_total at full extent
471                        // (predicate prevents the active-extent path).
472                        let mut p = *params;
473                        if p.bucket_outermost == 1 && p.out_dim_0 > 0 {
474                            let scaled_d0 = scale(p.out_dim_0);
475                            let inner = p.out_total / p.out_dim_0;
476                            p.out_total = scaled_d0 * inner;
477                        }
478                        dev.queue
479                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
480                    }
481                    Step::Narrow { params } => {
482                        let mut p = *params;
483                        p.total = scale(p.total);
484                        dev.queue
485                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
486                    }
487                    Step::Concat { params } => {
488                        let mut p = *params;
489                        p.total = scale(p.total);
490                        dev.queue
491                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
492                    }
493                    Step::Gather { params } => {
494                        let mut p = *params;
495                        p.n_out = scale(p.n_out);
496                        dev.queue
497                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
498                    }
499                    Step::GatherAxis { params } => {
500                        let mut p = *params;
501                        p.total = scale(p.total);
502                        dev.queue
503                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
504                    }
505                    Step::Attention { params, .. } => {
506                        // PLAN L1: scale seq_q + seq_k. Stride fields
507                        // (seq_q_stride / seq_k_stride) stay at the
508                        // compile-time full extent, so per-(batch, head)
509                        // offset math in the WGSL stays correct.
510                        let mut p = *params;
511                        p.seq_q = scale(p.seq_q);
512                        p.seq_k = scale(p.seq_k);
513                        dev.queue
514                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
515                    }
516                    Step::AttentionBackward { params, .. } => {
517                        let mut p = *params;
518                        if p.wrt == 0 {
519                            p.seq_q = scale(p.seq_q);
520                        } else {
521                            p.seq_k = scale(p.seq_k);
522                        }
523                        dev.queue
524                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
525                    }
526                    Step::Rope { params } => {
527                        // PLAN L1: scale `seq` and `n_total` proportionally.
528                        // `seq_stride` and `batch` stay at compile-time
529                        // values; the WGSL kernel uses them for buffer
530                        // offsets while `seq` / `n_total` are loop bounds.
531                        let mut p = *params;
532                        let s_active = scale(p.seq);
533                        p.seq = s_active;
534                        p.n_total = p.batch * s_active * p.last_dim;
535                        dev.queue
536                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
537                    }
538                    Step::Expand { params, .. } => {
539                        // PLAN L1: same pattern as Transpose.
540                        let mut p = *params;
541                        if p.bucket_outermost == 1 && p.out_dim_0 > 0 {
542                            let scaled_d0 = scale(p.out_dim_0);
543                            let inner = p.out_total / p.out_dim_0;
544                            p.out_total = scaled_d0 * inner;
545                        }
546                        dev.queue
547                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
548                    }
549                    Step::Argmax { params } => {
550                        let mut p = *params;
551                        p.outer = scale(p.outer);
552                        dev.queue
553                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
554                    }
555                    Step::Pool2d { params } => {
556                        let mut p = *params;
557                        p.n = scale(p.n);
558                        dev.queue
559                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
560                    }
561                    Step::Conv2d { params } | Step::Conv2dTiled { params } => {
562                        let mut p = *params;
563                        p.n = scale(p.n);
564                        dev.queue
565                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
566                    }
567                    Step::Pool1d { params } => {
568                        let mut p = *params;
569                        p.n = scale(p.n);
570                        dev.queue
571                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
572                    }
573                    Step::Pool3d { params } => {
574                        let mut p = *params;
575                        p.n = scale(p.n);
576                        dev.queue
577                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
578                    }
579                    Step::Conv1d { params } => {
580                        let mut p = *params;
581                        p.n = scale(p.n);
582                        dev.queue
583                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
584                    }
585                    Step::Conv3d { params } => {
586                        let mut p = *params;
587                        p.n = scale(p.n);
588                        dev.queue
589                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
590                    }
591                    Step::ScatterAdd { params } => {
592                        // Two-phase: phase 0 zeros the FULL output (preserves
593                        // accumulator semantics); phase 1 scatters first
594                        // num_updates_active updates only.
595                        let mut p = *params;
596                        if p.op == 1 {
597                            p.num_updates = scale(p.num_updates);
598                        }
599                        dev.queue
600                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
601                    }
602                    Step::TopK { params } => {
603                        let mut p = *params;
604                        p.outer = scale(p.outer);
605                        dev.queue
606                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
607                    }
608                    Step::WelchPeaksGpu { params } => {
609                        let mut p = *params;
610                        p.welch_batch = scale(p.welch_batch);
611                        dev.queue
612                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
613                    }
614                    Step::UmapKnn { params } => {
615                        let mut p = *params;
616                        p.n = scale(p.n);
617                        dev.queue
618                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
619                    }
620                    Step::GroupedMatmul { params } => {
621                        let mut p = *params;
622                        p.m = scale(p.m);
623                        dev.queue
624                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
625                    }
626                    Step::Sample { params } => {
627                        let mut p = *params;
628                        p.outer = scale(p.outer);
629                        dev.queue
630                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
631                    }
632                    Step::SelectiveScan { params } => {
633                        // Predicate-gated to batch=1: scale seq.
634                        let mut p = *params;
635                        p.seq = scale(p.seq);
636                        dev.queue
637                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
638                    }
639                    Step::Mamba2 { params } => {
640                        let mut p = *params;
641                        p.seq = scale(p.seq);
642                        dev.queue
643                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
644                    }
645                    Step::Gru { params } => {
646                        let mut p = *params;
647                        p.seq = scale(p.seq);
648                        dev.queue
649                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
650                    }
651                    Step::Rnn { params } => {
652                        let mut p = *params;
653                        p.seq = scale(p.seq);
654                        dev.queue
655                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
656                    }
657                    Step::DequantMatmul { params } => {
658                        let mut p = *params;
659                        p.m = scale(p.m);
660                        dev.queue
661                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
662                    }
663                    // Im2ColGpu params are static (written once at compile);
664                    // no active-extent scaling.
665                    Step::Im2ColGpu { .. }
666                    | Step::GatherSplit { .. }
667                    | Step::DequantMatmulGguf { .. }
668                    | Step::DequantGroupedMatmulGguf { .. }
669                    | Step::GatedDeltaNet { .. }
670                    | Step::Lstm { .. }
671                    | Step::ConvTranspose2d { .. }
672                    | Step::GroupNormHost { .. }
673                    | Step::LayerNorm2dHost { .. }
674                    | Step::ResizeNearest2xHost { .. }
675                    | Step::ReverseHost { .. }
676                    | Step::ArgReduceHost { .. }
677                    | Step::GruHost { .. }
678                    | Step::RnnHost { .. }
679                    | Step::Llada2GroupLimitedGate { .. }
680                    | Step::UmapKnnHost { .. }
681                    | Step::MsDeformAttnHost { .. }
682                    | Step::FftHost { .. }
683                    | Step::ScanHost { .. }
684                    | Step::Im2ColHost { .. }
685                    | Step::RngNormalHost { .. }
686                    | Step::RngUniformHost { .. }
687                    | Step::WelchPeaksHost { .. }
688                    | Step::LogMelHost { .. }
689                    | Step::LogMelBackwardHost { .. } => {}
690                    Step::FusedResidualLn { params } => {
691                        let mut p = *params;
692                        p.outer = scale(p.outer);
693                        dev.queue
694                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
695                    }
696                    Step::FusedResidualLnTee { params } => {
697                        let mut p = *params;
698                        p.outer = scale(p.outer);
699                        dev.queue
700                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
701                    }
702                    Step::FusedResidualRmsNorm { params } => {
703                        let mut p = *params;
704                        p.outer = scale(p.outer);
705                        dev.queue
706                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
707                    }
708                    Step::MatmulQkv { params, kind: _ } => {
709                        let mut p = *params;
710                        p.m = scale(p.m);
711                        dev.queue
712                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
713                    }
714                    #[cfg(feature = "splat")]
715                    Step::GaussianSplatRender { .. }
716                    | Step::GaussianSplatRenderBackward { .. }
717                    | Step::GaussianSplatPrepare { .. }
718                    | Step::GaussianSplatRasterize { .. } => {}
719                }
720                if !matches!(step, Step::FftGpu { .. }) {
721                    gpu_ui += 1;
722                }
723            }
724            self.uniforms_active_extent = Some(active);
725        }
726
727        // Encode + submit.
728        let mm_k = matmul_kernel(&dev.device);
729        let mm_w_active = matmul_wide_active_kernel(&dev.device);
730        let mm_f16w = matmul_f16w_kernel(&dev.device);
731        let mm_f16c = matmul_f16_compute_kernel(&dev.device);
732        let mm_coop = matmul_coop16_kernel(&dev.device);
733        let mm_coop_f16_vk = matmul_coop_f16_vulkan_kernel(&dev.device);
734        let mm_coop_f32 = matmul_coop_f32_active_kernel(&dev.device);
735        let mm_cast = cast_f32_to_f16_kernel(&dev.device);
736        let bk = binary_kernel(&dev.device);
737        let uk = unary_kernel(&dev.device);
738        let ck = compare_kernel(&dev.device);
739        let wk = where_kernel(&dev.device);
740        let fk = fma_kernel(&dev.device);
741        let mut step_i = 0;
742        let mut gpu_bi = 0usize;
743        let mut fft_i = 0usize;
744        while step_i < self.schedule.len() {
745            let mut enc = dev
746                .device
747                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
748                    label: Some("rlx-wgpu run"),
749                });
750            {
751                let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
752                    label: Some("rlx-wgpu compute pass"),
753                    timestamp_writes: None,
754                });
755                let mut pass_dispatched = false;
756                while step_i < self.schedule.len() {
757                    if step_is_tail_host(&self.schedule[step_i]) {
758                        step_i += 1;
759                        continue;
760                    }
761                    if step_runs_on_host(&self.schedule[step_i]) {
762                        break;
763                    }
764                    // Vulkan/DX12: end the pass after unary/cast so f32→f16
765                    // mirrors are visible to the next step. Only split once
766                    // we've dispatched in *this* pass — otherwise the step that
767                    // needs the flush would never run (infinite empty passes).
768                    if pass_dispatched
769                        && step_i > 0
770                        && step_needs_pass_flush(&self.schedule[step_i], &self.schedule[step_i - 1])
771                    {
772                        break;
773                    }
774                    let step = &self.schedule[step_i];
775                    // PLAN L3: per-step Perfetto trace span; no-op when
776                    // env var RLX_TRACE_PERFETTO unset.
777                    let _perf = rlx_ir::perfetto::TraceSpan::new(step_name(step), "wgpu");
778                    match step {
779                        Step::CastF32ToF16 { params } => {
780                            // Pre-pass for matmul_coop16: mirror f32 arena
781                            // region into f16 shadow buffer so the matmul
782                            // kernel can read A as f16. One thread per
783                            // element; 64-thread workgroups.
784                            if let Some(cast_k) = mm_cast {
785                                pass.set_pipeline(&cast_k.pipeline);
786                                pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
787                                let (gx, gy, gz) = dispatch_dims(params.len, 64);
788                                pass.dispatch_workgroups(gx, gy, gz);
789                            }
790                        }
791                        Step::Matmul {
792                            m,
793                            n,
794                            batch,
795                            b_off_f32,
796                            b_is_param,
797                            compute_precision,
798                            ..
799                        } =>
800                        // The dispatch branches below use a chain of
801                        // `is_some() && …unwrap()` to pick a pipeline
802                        // because each variant cares about a different
803                        // Option<Pipeline>. `if let Some(p) = …` chains
804                        // would require nesting per variant; the flat
805                        // form is the readable shape here.
806                        {
807                            #[allow(clippy::unnecessary_unwrap)]
808                            // Safe at any batch (see safe_for_active_extent
809                            // comment); scale m, output rows past m_s per
810                            // batch retain prior values via c_batch_stride.
811                            let m_s = scale(*m);
812                            if m_s == 0 {
813                                continue;
814                            }
815                            let coop_f16_wide = mm_coop_f16_vk.is_some()
816                                && *compute_precision == MatmulCompute::CoopF16Vk
817                                && crate::coop_f16_vk::use_wide_matmul(
818                                    *b_off_f32,
819                                    *n,
820                                    &self.coop_f16_b_param,
821                                    &self.coop_f16_vk_wide_b,
822                                );
823                            pass.set_bind_group(
824                                0,
825                                coop_f16_vk_bind_group(self, gpu_bi, coop_f16_wide),
826                                &[],
827                            );
828                            // Kernel selection priority:
829                            //   1. compute_precision == F16 + b_is_param +
830                            //      SHADER_F16 → matmul_f16_compute
831                            //      (f16 multiply, f32 acc — 2× ALU on Apple)
832                            //   2. legacy RLX_WGPU_F16_WEIGHTS opt-in →
833                            //      matmul_f16w (storage-only f16; experimental,
834                            //      currently regresses on Apple)
835                            //   3. wide-N (m≥32, n≥64)   → matmul_wide
836                            //   4. otherwise            → matmul (small/skinny)
837                            let f16w_opt_in = rlx_ir::env::flag("RLX_WGPU_F16_WEIGHTS");
838                            if let Some(coop) = mm_coop.as_ref()
839                                && *b_is_param
840                                && *compute_precision == MatmulCompute::Coop16
841                            {
842                                // Hardware GEMM via simdgroup_matrix /
843                                // KHR_cooperative_matrix. 32×32 output tile
844                                // per workgroup (16 hardware-GEMM ops with
845                                // shared A/B loads). Caller guaranteed m, n,
846                                // k are multiples of 32/32/8.
847                                pass.set_pipeline(&coop.pipeline);
848                                pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), *batch);
849                            } else if mm_coop_f16_vk.is_some()
850                                && *compute_precision == MatmulCompute::CoopF16Vk
851                            {
852                                if coop_f16_wide {
853                                    dispatch_wide_f32_matmul(
854                                        &mut pass,
855                                        mm_w_active,
856                                        mm_k,
857                                        m_s,
858                                        *n,
859                                        *batch,
860                                    );
861                                } else {
862                                    let n_eff = scale(*n);
863                                    let coop_vk =
864                                        matmul_coop_f16_vulkan_active_kernel(&dev.device, n_eff)
865                                            .expect("coop f16 vk kernel missing");
866                                    pass.set_pipeline(&coop_vk.pipeline);
867                                    pass.dispatch_workgroups(
868                                        m_s.div_ceil(16),
869                                        n.div_ceil(16),
870                                        *batch,
871                                    );
872                                }
873                            } else if let Some(coop_f32) = mm_coop_f32.as_ref()
874                                && *b_is_param
875                                && *compute_precision == MatmulCompute::CoopF32
876                            {
877                                // CoopF32: Metal uses 32×32 simdgroup tiles;
878                                // Vulkan uses 8×8 coopLoadT portable kernel.
879                                pass.set_pipeline(&coop_f32.pipeline);
880                                let backend = wgpu_device()
881                                    .map(|d| d.backend)
882                                    .unwrap_or(wgpu::Backend::Noop);
883                                let (gx, gy) = if backend == wgpu::Backend::Metal {
884                                    (n.div_ceil(32), m_s.div_ceil(32))
885                                } else {
886                                    (m_s.div_ceil(8), n.div_ceil(8))
887                                };
888                                pass.dispatch_workgroups(gx, gy, *batch);
889                            } else if let Some(f16c) = mm_f16c.as_ref()
890                                && *b_is_param
891                                && *compute_precision == MatmulCompute::F16
892                            {
893                                pass.set_pipeline(&f16c.pipeline);
894                                pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), *batch);
895                            } else if let Some(f16w) = mm_f16w.as_ref()
896                                && *b_is_param
897                                && f16w_opt_in
898                            {
899                                pass.set_pipeline(&f16w.pipeline);
900                                pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), *batch);
901                            } else if m_s >= 32 && *n >= 64 {
902                                pass.set_pipeline(&mm_w_active.pipeline);
903                                let backend = wgpu_device()
904                                    .map(|d| d.backend)
905                                    .unwrap_or(wgpu::Backend::Noop);
906                                let (gx, gy) = if matches!(
907                                    backend,
908                                    wgpu::Backend::Vulkan | wgpu::Backend::Dx12
909                                ) {
910                                    (n.div_ceil(64), m_s.div_ceil(64))
911                                } else {
912                                    (n.div_ceil(64), m_s.div_ceil(32))
913                                };
914                                pass.dispatch_workgroups(gx, gy, *batch);
915                            } else {
916                                pass.set_pipeline(&mm_k.pipeline);
917                                pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), *batch);
918                            }
919                        }
920                        Step::Binary { params } => {
921                            let n_s = scale(params.n);
922                            if n_s == 0 {
923                                continue;
924                            }
925                            pass.set_pipeline(&bk.pipeline);
926                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
927                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
928                            pass.dispatch_workgroups(gx, gy, gz);
929                        }
930                        Step::Compare { params } => {
931                            let n_s = scale(params.n);
932                            if n_s == 0 {
933                                continue;
934                            }
935                            pass.set_pipeline(&ck.pipeline);
936                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
937                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
938                            pass.dispatch_workgroups(gx, gy, gz);
939                        }
940                        Step::Unary { params, f16_mirror } => {
941                            let n_s = scale(params.n);
942                            if n_s == 0 {
943                                continue;
944                            }
945                            if *f16_mirror {
946                                if let Some(uk_f16) = unary_f16_mirror_kernel(&dev.device) {
947                                    pass.set_pipeline(&uk_f16.pipeline);
948                                } else {
949                                    pass.set_pipeline(&uk.pipeline);
950                                }
951                            } else {
952                                pass.set_pipeline(&uk.pipeline);
953                            }
954                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
955                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
956                            pass.dispatch_workgroups(gx, gy, gz);
957                        }
958                        Step::Where { params } => {
959                            let n_s = scale(params.n);
960                            if n_s == 0 {
961                                continue;
962                            }
963                            pass.set_pipeline(&wk.pipeline);
964                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
965                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
966                            pass.dispatch_workgroups(gx, gy, gz);
967                        }
968                        Step::Fma { params } => {
969                            let n_s = scale(params.n);
970                            if n_s == 0 {
971                                continue;
972                            }
973                            pass.set_pipeline(&fk.pipeline);
974                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
975                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
976                            pass.dispatch_workgroups(gx, gy, gz);
977                        }
978                        Step::Reduce { params } => {
979                            let outer_s = scale(params.outer);
980                            if outer_s == 0 {
981                                continue;
982                            }
983                            let rk = reduce_kernel(&dev.device);
984                            pass.set_pipeline(&rk.pipeline);
985                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
986                            let total_out = outer_s.saturating_mul(params.inner);
987                            if params.reduce_dim <= 64 {
988                                // Fast path: 1 thread per output cell.
989                                let (gx, gy, gz) = dispatch_dims(total_out, 64);
990                                pass.dispatch_workgroups(gx, gy, gz);
991                            } else {
992                                // Tree-reduce path: 1 workgroup (64
993                                // threads) per output cell, parallel
994                                // reduction with shared scratch.
995                                let (gx, gy, gz) = dispatch_dims(total_out, 1);
996                                pass.dispatch_workgroups(gx, gy, gz);
997                            }
998                        }
999                        Step::Softmax { params } => {
1000                            let outer_s = scale(params.outer);
1001                            if outer_s == 0 {
1002                                continue;
1003                            }
1004                            let sk = softmax_kernel(&dev.device);
1005                            pass.set_pipeline(&sk.pipeline);
1006                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1007                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1008                            pass.dispatch_workgroups(gx, gy, gz);
1009                        }
1010                        Step::SoftmaxCrossEntropy { params } => {
1011                            let outer_s = scale(params.outer);
1012                            if outer_s == 0 {
1013                                continue;
1014                            }
1015                            let sk = softmax_cross_entropy_kernel(&dev.device);
1016                            pass.set_pipeline(&sk.pipeline);
1017                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1018                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1019                            pass.dispatch_workgroups(gx, gy, gz);
1020                        }
1021                        Step::LayerNorm { params } => {
1022                            let outer_s = scale(params.outer);
1023                            if outer_s == 0 {
1024                                continue;
1025                            }
1026                            let lk = layernorm_kernel(&dev.device);
1027                            pass.set_pipeline(&lk.pipeline);
1028                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1029                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1030                            pass.dispatch_workgroups(gx, gy, gz);
1031                        }
1032                        Step::RmsNormBackwardInput { params } => {
1033                            let outer_s = scale(params.outer);
1034                            if outer_s == 0 {
1035                                continue;
1036                            }
1037                            let rk = rms_norm_backward_kernel(&dev.device);
1038                            pass.set_pipeline(&rk.pipeline);
1039                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1040                            pass.dispatch_workgroups(outer_s, 1, 1);
1041                        }
1042                        Step::RmsNormBackwardGamma { params }
1043                        | Step::RmsNormBackwardBeta { params } => {
1044                            if params.inner == 0 {
1045                                continue;
1046                            }
1047                            let rk = rms_norm_backward_param_kernel(&dev.device);
1048                            pass.set_pipeline(&rk.pipeline);
1049                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1050                            pass.dispatch_workgroups(1, 1, 1);
1051                        }
1052                        Step::LayerNormBackwardInput { params } => {
1053                            let outer_s = scale(params.outer);
1054                            if outer_s == 0 {
1055                                continue;
1056                            }
1057                            let lk = layer_norm_backward_input_kernel(&dev.device);
1058                            pass.set_pipeline(&lk.pipeline);
1059                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1060                            pass.dispatch_workgroups(outer_s, 1, 1);
1061                        }
1062                        Step::LayerNormBackwardGammaPartial {
1063                            params,
1064                            num_workgroups,
1065                        } => {
1066                            if params.inner == 0 || *num_workgroups == 0 {
1067                                continue;
1068                            }
1069                            let lk = layer_norm_backward_gamma_partial_kernel(&dev.device);
1070                            pass.set_pipeline(&lk.pipeline);
1071                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1072                            pass.dispatch_workgroups(*num_workgroups, 1, 1);
1073                        }
1074                        Step::LayerNormBackwardGammaReduce { params } => {
1075                            if params.inner == 0 {
1076                                continue;
1077                            }
1078                            let lk = layer_norm_backward_gamma_reduce_kernel(&dev.device);
1079                            pass.set_pipeline(&lk.pipeline);
1080                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1081                            pass.dispatch_workgroups(1, 1, 1);
1082                        }
1083                        Step::CumsumBackward { params } => {
1084                            let outer_s = scale(params.outer);
1085                            if outer_s == 0 {
1086                                continue;
1087                            }
1088                            let ck = cumsum_backward_kernel(&dev.device);
1089                            pass.set_pipeline(&ck.pipeline);
1090                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1091                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1092                            pass.dispatch_workgroups(gx, gy, gz);
1093                        }
1094                        Step::RopeBackward { params } => {
1095                            let seq_s = scale(params.seq);
1096                            if seq_s == 0 {
1097                                continue;
1098                            }
1099                            let rk = rope_backward_kernel(&dev.device);
1100                            pass.set_pipeline(&rk.pipeline);
1101                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1102                            let total = params.batch * seq_s * params.hidden;
1103                            let (gx, gy, gz) = dispatch_dims(total, 64);
1104                            pass.dispatch_workgroups(gx, gy, gz);
1105                        }
1106                        Step::GatherBackward { params } => {
1107                            let outer_s = scale(params.outer);
1108                            if outer_s == 0 {
1109                                continue;
1110                            }
1111                            let total = outer_s * params.axis_dim * params.trailing;
1112                            if total > 0 {
1113                                let zk = gather_backward_zero_kernel(&dev.device);
1114                                pass.set_pipeline(&zk.pipeline);
1115                                pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1116                                let (gx, _, _) = dispatch_dims(total, 256);
1117                                pass.dispatch_workgroups(gx, 1, 1);
1118                            }
1119                            let ak = gather_backward_acc_kernel(&dev.device);
1120                            pass.set_pipeline(&ak.pipeline);
1121                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1122                            pass.dispatch_workgroups(outer_s, 1, 1);
1123                        }
1124                        Step::Cumsum { params } => {
1125                            let outer_s = scale(params.outer);
1126                            if outer_s == 0 {
1127                                continue;
1128                            }
1129                            let ck2 = cumsum_kernel(&dev.device);
1130                            pass.set_pipeline(&ck2.pipeline);
1131                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1132                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1133                            pass.dispatch_workgroups(gx, gy, gz);
1134                        }
1135                        Step::FftGpu {
1136                            src_off,
1137                            dst_off,
1138                            outer,
1139                            n,
1140                            inverse,
1141                            norm_scale,
1142                        } => {
1143                            let res = &self.fft_gpu_steps[fft_i];
1144                            fft_i += 1;
1145                            crate::fft_dispatch::dispatch_fft_gpu_in_pass(
1146                                &dev.device,
1147                                &dev.queue,
1148                                &mut pass,
1149                                res,
1150                                *src_off,
1151                                *dst_off,
1152                                *outer,
1153                                *n,
1154                                *inverse != 0,
1155                                *norm_scale,
1156                            );
1157                        }
1158                        Step::Copy { params } => {
1159                            let n_s = scale(params.n);
1160                            if n_s == 0 {
1161                                continue;
1162                            }
1163                            let ck2 = copy_kernel(&dev.device);
1164                            pass.set_pipeline(&ck2.pipeline);
1165                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1166                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
1167                            pass.dispatch_workgroups(gx, gy, gz);
1168                        }
1169                        Step::BufferCopy { .. } => {
1170                            // Host step: `copy_buffer_to_buffer` runs outside compute passes.
1171                        }
1172                        Step::ElementwiseRegion { params } => {
1173                            let len_s = scale(params.len);
1174                            if len_s == 0 {
1175                                continue;
1176                            }
1177                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1178                            if params.prologue == rlx_ir::REGION_PROLOGUE_RESIZE_NEAREST_2X_NCHW {
1179                                let ek = elementwise_region_spatial_kernel(&dev.device);
1180                                pass.set_pipeline(&ek.pipeline);
1181                                let (gx, gy, gz) = dispatch_prologue_nchw(
1182                                    params.out_w,
1183                                    params.out_h,
1184                                    params.out_n * params.out_c,
1185                                );
1186                                pass.dispatch_workgroups(gx, gy, gz);
1187                            } else {
1188                                let ek = elementwise_region_kernel(&dev.device);
1189                                pass.set_pipeline(&ek.pipeline);
1190                                let (gx, gy, gz) = dispatch_dims(len_s, 64);
1191                                pass.dispatch_workgroups(gx, gy, gz);
1192                            }
1193                        }
1194                        Step::BatchElementwiseRegion { params } => {
1195                            let slice_len_s = scale(params.slice_len);
1196                            let num_batch_s = scale(params.num_batch);
1197                            if slice_len_s == 0 || num_batch_s == 0 {
1198                                continue;
1199                            }
1200                            let ek = batch_elementwise_region_kernel(&dev.device);
1201                            pass.set_pipeline(&ek.pipeline);
1202                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1203                            let (gx, gy, _) = dispatch_dims(slice_len_s, 64);
1204                            pass.dispatch_workgroups(gx, gy, num_batch_s);
1205                        }
1206                        Step::Transpose { params, .. } => {
1207                            // Compute scaled grid count to match the
1208                            // uniform's scaled out_total when bucket axis
1209                            // is outermost.
1210                            let total_s = if params.bucket_outermost == 1 && params.out_dim_0 > 0 {
1211                                let scaled_d0 = scale(params.out_dim_0);
1212                                let inner = params.out_total / params.out_dim_0;
1213                                scaled_d0 * inner
1214                            } else {
1215                                params.out_total
1216                            };
1217                            if total_s == 0 {
1218                                continue;
1219                            }
1220                            let tk = transpose_kernel(&dev.device);
1221                            pass.set_pipeline(&tk.pipeline);
1222                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1223                            let (gx, gy, gz) = dispatch_dims(total_s, 64);
1224                            pass.dispatch_workgroups(gx, gy, gz);
1225                        }
1226                        Step::Narrow { params } => {
1227                            let total_s = scale(params.total);
1228                            if total_s == 0 {
1229                                continue;
1230                            }
1231                            let nk = narrow_kernel(&dev.device);
1232                            pass.set_pipeline(&nk.pipeline);
1233                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1234                            let (gx, gy, gz) = dispatch_dims(total_s, 64);
1235                            pass.dispatch_workgroups(gx, gy, gz);
1236                        }
1237                        Step::Concat { params } => {
1238                            let total_s = scale(params.total);
1239                            if total_s == 0 {
1240                                continue;
1241                            }
1242                            let cck = concat_kernel(&dev.device);
1243                            pass.set_pipeline(&cck.pipeline);
1244                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1245                            let (gx, gy, gz) = dispatch_dims(total_s, 64);
1246                            pass.dispatch_workgroups(gx, gy, gz);
1247                        }
1248                        Step::Gather { params } => {
1249                            let n_out_s = scale(params.n_out);
1250                            if n_out_s == 0 {
1251                                continue;
1252                            }
1253                            let gk = gather_kernel(&dev.device);
1254                            pass.set_pipeline(&gk.pipeline);
1255                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1256                            let (gx, gy, gz) = dispatch_dims(n_out_s, 64);
1257                            pass.dispatch_workgroups(gx, gy, gz);
1258                        }
1259                        Step::GatherAxis { params } => {
1260                            let total_s = scale(params.total);
1261                            if total_s == 0 {
1262                                continue;
1263                            }
1264                            let gk = gather_axis_kernel(&dev.device);
1265                            pass.set_pipeline(&gk.pipeline);
1266                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1267                            let (gx, gy, gz) = dispatch_dims(total_s, 64);
1268                            pass.dispatch_workgroups(gx, gy, gz);
1269                        }
1270                        Step::Attention { params, .. } => {
1271                            // Scale seq_q for grid dim; per-head strides
1272                            // come from seq_q_stride / seq_k_stride (full
1273                            // extent) inside the WGSL.
1274                            let seq_q_s = scale(params.seq_q);
1275                            if seq_q_s == 0 {
1276                                continue;
1277                            }
1278                            let ak = attention_kernel(&dev.device);
1279                            pass.set_pipeline(&ak.pipeline);
1280                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1281                            let total = params.batch * params.heads * seq_q_s;
1282                            let (gx, gy, gz) = dispatch_dims(total, 64);
1283                            pass.dispatch_workgroups(gx, gy, gz);
1284                        }
1285                        Step::AttentionBackward { params, .. } => {
1286                            let axis = if params.wrt == 0 {
1287                                params.seq_q
1288                            } else {
1289                                params.seq_k
1290                            };
1291                            let axis_s = scale(axis);
1292                            if axis_s == 0 {
1293                                continue;
1294                            }
1295                            let ak = attention_bwd_kernel(&dev.device);
1296                            pass.set_pipeline(&ak.pipeline);
1297                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1298                            let total = params.batch * params.heads * axis_s;
1299                            let (gx, gy, gz) = dispatch_dims(total, 64);
1300                            pass.dispatch_workgroups(gx, gy, gz);
1301                        }
1302                        Step::Rope { params } => {
1303                            // Multi-batch via stride-field WGSL fix:
1304                            // iterate `batch * scaled_seq * last_dim` items.
1305                            let s_active = scale(params.seq);
1306                            let total_s = params.batch * s_active * params.last_dim;
1307                            if total_s == 0 {
1308                                continue;
1309                            }
1310                            let rk = rope_kernel(&dev.device);
1311                            pass.set_pipeline(&rk.pipeline);
1312                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1313                            let (gx, gy, gz) = dispatch_dims(total_s, 64);
1314                            pass.dispatch_workgroups(gx, gy, gz);
1315                        }
1316                        Step::Expand { params, .. } => {
1317                            let total_s = if params.bucket_outermost == 1 && params.out_dim_0 > 0 {
1318                                let scaled_d0 = scale(params.out_dim_0);
1319                                let inner = params.out_total / params.out_dim_0;
1320                                scaled_d0 * inner
1321                            } else {
1322                                params.out_total
1323                            };
1324                            if total_s == 0 {
1325                                continue;
1326                            }
1327                            let ek = expand_kernel(&dev.device);
1328                            pass.set_pipeline(&ek.pipeline);
1329                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1330                            let (gx, gy, gz) = dispatch_dims(total_s, 64);
1331                            pass.dispatch_workgroups(gx, gy, gz);
1332                        }
1333                        Step::Argmax { params } => {
1334                            let outer_s = scale(params.outer);
1335                            if outer_s == 0 {
1336                                continue;
1337                            }
1338                            let amk = argmax_kernel(&dev.device);
1339                            pass.set_pipeline(&amk.pipeline);
1340                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1341                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1342                            pass.dispatch_workgroups(gx, gy, gz);
1343                        }
1344                        Step::Pool2d { params } => {
1345                            let n_s = scale(params.n);
1346                            if n_s == 0 {
1347                                continue;
1348                            }
1349                            let pk = pool2d_kernel(&dev.device);
1350                            pass.set_pipeline(&pk.pipeline);
1351                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1352                            let total = n_s * params.c * params.h_out * params.w_out;
1353                            let (gx, gy, gz) = dispatch_dims(total, 64);
1354                            pass.dispatch_workgroups(gx, gy, gz);
1355                        }
1356                        Step::Conv2d { params } => {
1357                            let n_s = scale(params.n);
1358                            if n_s == 0 {
1359                                continue;
1360                            }
1361                            let ck2 = conv2d_kernel(&dev.device);
1362                            pass.set_pipeline(&ck2.pipeline);
1363                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1364                            // conv2d.wgsl tiles `CONV2D_TILE` output spatial
1365                            // positions per thread (const must match the kernel).
1366                            let spatial = params.h_out * params.w_out;
1367                            let sp_tiles = spatial.div_ceil(CONV2D_TILE);
1368                            let total = n_s * params.c_out * sp_tiles;
1369                            let (gx, gy, gz) = dispatch_dims(total, 64);
1370                            pass.dispatch_workgroups(gx, gy, gz);
1371                        }
1372                        Step::Conv2dTiled { params } => {
1373                            let n_s = scale(params.n);
1374                            if n_s == 0 {
1375                                continue;
1376                            }
1377                            let ck = conv1d_tiled_kernel(&dev.device);
1378                            pass.set_pipeline(&ck.pipeline);
1379                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1380                            // conv1d_tiled.wgsl: one thread per output element
1381                            // (c_out * l_out), N == 1, w_out == 1.
1382                            let total = n_s * params.c_out * params.h_out;
1383                            let (gx, gy, gz) = dispatch_dims(total, 64);
1384                            pass.dispatch_workgroups(gx, gy, gz);
1385                        }
1386                        Step::Im2ColGpu { params } => {
1387                            // One thread per col element (k_total * spatial);
1388                            // the following Step::Matmul consumes the col matrix.
1389                            let imk = im2col2d_kernel(&dev.device);
1390                            pass.set_pipeline(&imk.pipeline);
1391                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1392                            let total = params.k_total.saturating_mul(params.spatial);
1393                            let (gx, gy, gz) = dispatch_dims(total, 64);
1394                            pass.dispatch_workgroups(gx, gy, gz);
1395                        }
1396                        Step::Pool1d { params } => {
1397                            let n_s = scale(params.n);
1398                            if n_s == 0 {
1399                                continue;
1400                            }
1401                            let pk = pool1d_kernel(&dev.device);
1402                            pass.set_pipeline(&pk.pipeline);
1403                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1404                            let total = n_s * params.c * params.l_out;
1405                            let (gx, gy, gz) = dispatch_dims(total, 64);
1406                            pass.dispatch_workgroups(gx, gy, gz);
1407                        }
1408                        Step::Pool3d { params } => {
1409                            let n_s = scale(params.n);
1410                            if n_s == 0 {
1411                                continue;
1412                            }
1413                            let pk = pool3d_kernel(&dev.device);
1414                            pass.set_pipeline(&pk.pipeline);
1415                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1416                            let total = n_s * params.c * params.d_out * params.h_out * params.w_out;
1417                            let (gx, gy, gz) = dispatch_dims(total, 64);
1418                            pass.dispatch_workgroups(gx, gy, gz);
1419                        }
1420                        Step::Conv1d { params } => {
1421                            let n_s = scale(params.n);
1422                            if n_s == 0 {
1423                                continue;
1424                            }
1425                            let ck = conv1d_kernel(&dev.device);
1426                            pass.set_pipeline(&ck.pipeline);
1427                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1428                            let total = n_s * params.c_out * params.l_out;
1429                            let (gx, gy, gz) = dispatch_dims(total, 64);
1430                            pass.dispatch_workgroups(gx, gy, gz);
1431                        }
1432                        Step::Conv3d { params } => {
1433                            let n_s = scale(params.n);
1434                            if n_s == 0 {
1435                                continue;
1436                            }
1437                            let ck = conv3d_kernel(&dev.device);
1438                            pass.set_pipeline(&ck.pipeline);
1439                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1440                            let total =
1441                                n_s * params.c_out * params.d_out * params.h_out * params.w_out;
1442                            let (gx, gy, gz) = dispatch_dims(total, 64);
1443                            pass.dispatch_workgroups(gx, gy, gz);
1444                        }
1445                        Step::ScatterAdd { params } => {
1446                            let sk = scatter_add_kernel(&dev.device);
1447                            pass.set_pipeline(&sk.pipeline);
1448                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1449                            // Phase 0 zeros the FULL output (preserves
1450                            // accumulator semantics). Phase 1 scatters first
1451                            // num_updates_active updates only; serial single
1452                            // workgroup either way (atomic CAS unsupported in
1453                            // naga's MSL emitter — see scatter_add.wgsl).
1454                            if params.op == 0 {
1455                                let (gx, gy, gz) = dispatch_dims(params.out_total, 64);
1456                                pass.dispatch_workgroups(gx, gy, gz);
1457                            } else {
1458                                pass.dispatch_workgroups(1, 1, 1);
1459                            }
1460                        }
1461                        Step::TopK { params } => {
1462                            let outer_s = scale(params.outer);
1463                            if outer_s == 0 {
1464                                continue;
1465                            }
1466                            let tk = topk_kernel(&dev.device);
1467                            pass.set_pipeline(&tk.pipeline);
1468                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1469                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1470                            pass.dispatch_workgroups(gx, gy, gz);
1471                        }
1472                        Step::WelchPeaksGpu { params } => {
1473                            let batch_s = scale(params.welch_batch);
1474                            if batch_s == 0 {
1475                                continue;
1476                            }
1477                            let wk = welch_peaks_gpu_kernel(&dev.device);
1478                            pass.set_pipeline(&wk.pipeline);
1479                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1480                            let (gx, gy, gz) = dispatch_dims(batch_s, 64);
1481                            pass.dispatch_workgroups(gx, gy, gz);
1482                        }
1483                        Step::UmapKnn { params } => {
1484                            let n_s = scale(params.n);
1485                            if n_s == 0 {
1486                                continue;
1487                            }
1488                            let uk = umap_knn_kernel(&dev.device);
1489                            pass.set_pipeline(&uk.pipeline);
1490                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1491                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
1492                            pass.dispatch_workgroups(gx, gy, gz);
1493                        }
1494                        Step::GroupedMatmul { params } => {
1495                            let m_s = scale(params.m);
1496                            if m_s == 0 {
1497                                continue;
1498                            }
1499                            let gk = grouped_matmul_kernel(&dev.device);
1500                            pass.set_pipeline(&gk.pipeline);
1501                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1502                            pass.dispatch_workgroups(params.n.div_ceil(8), m_s.div_ceil(8), 1);
1503                        }
1504                        Step::Sample { params } => {
1505                            let outer_s = scale(params.outer);
1506                            if outer_s == 0 {
1507                                continue;
1508                            }
1509                            let sk = sample_kernel(&dev.device);
1510                            pass.set_pipeline(&sk.pipeline);
1511                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1512                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1513                            pass.dispatch_workgroups(gx, gy, gz);
1514                        }
1515                        Step::SelectiveScan { params } => {
1516                            // Predicate-gated to batch=1; the seq scaling
1517                            // happens inside the kernel (uniform sees scaled
1518                            // seq). Dispatch grid here is per-(batch, hidden);
1519                            // unaffected by seq scaling.
1520                            let ssk = selective_scan_kernel(&dev.device);
1521                            pass.set_pipeline(&ssk.pipeline);
1522                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1523                            let total = params.batch * params.hidden;
1524                            let (gx, gy, gz) = dispatch_dims(total, 64);
1525                            pass.dispatch_workgroups(gx, gy, gz);
1526                        }
1527                        Step::Mamba2 { params } => {
1528                            let mk = mamba2_kernel(&dev.device);
1529                            pass.set_pipeline(&mk.pipeline);
1530                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1531                            let total = params.batch * params.heads * params.head_dim;
1532                            let (gx, gy, gz) = dispatch_dims(total, 64);
1533                            pass.dispatch_workgroups(gx, gy, gz);
1534                        }
1535                        Step::Gru { params } => {
1536                            // One workgroup per batch item (workgroup_size=256).
1537                            let gk = gru_kernel(&dev.device);
1538                            pass.set_pipeline(&gk.pipeline);
1539                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1540                            pass.dispatch_workgroups(params.batch, 1, 1);
1541                        }
1542                        Step::Rnn { params } => {
1543                            let rk = rnn_kernel(&dev.device);
1544                            pass.set_pipeline(&rk.pipeline);
1545                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1546                            pass.dispatch_workgroups(params.batch, 1, 1);
1547                        }
1548                        Step::DequantMatmul { params } => {
1549                            let m_s = scale(params.m);
1550                            if m_s == 0 {
1551                                continue;
1552                            }
1553                            let dk = dequant_matmul_kernel(&dev.device);
1554                            pass.set_pipeline(&dk.pipeline);
1555                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1556                            pass.dispatch_workgroups(params.n.div_ceil(8), m_s.div_ceil(8), 1);
1557                        }
1558                        Step::FusedResidualLn { params } => {
1559                            let outer_s = scale(params.outer);
1560                            if outer_s == 0 {
1561                                continue;
1562                            }
1563                            let frk = fused_residual_ln_kernel(&dev.device);
1564                            pass.set_pipeline(&frk.pipeline);
1565                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1566                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1567                            pass.dispatch_workgroups(gx, gy, gz);
1568                        }
1569                        Step::FusedResidualLnTee { params } => {
1570                            let outer_s = scale(params.outer);
1571                            if outer_s == 0 {
1572                                continue;
1573                            }
1574                            let frtk = fused_residual_ln_tee_kernel(&dev.device);
1575                            pass.set_pipeline(&frtk.pipeline);
1576                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1577                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1578                            pass.dispatch_workgroups(gx, gy, gz);
1579                        }
1580                        Step::FusedResidualRmsNorm { params } => {
1581                            let outer_s = scale(params.outer);
1582                            if outer_s == 0 {
1583                                continue;
1584                            }
1585                            let frk = fused_residual_rms_norm_kernel(&dev.device);
1586                            pass.set_pipeline(&frk.pipeline);
1587                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1588                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1589                            pass.dispatch_workgroups(gx, gy, gz);
1590                        }
1591                        Step::MatmulQkv { params, kind } => {
1592                            let m_s = scale(params.m);
1593                            if m_s == 0 {
1594                                continue;
1595                            }
1596                            let qkv_coop_wide = matches!(kind, MatmulQkvKind::CoopF16Vk)
1597                                && crate::coop_f16_vk::use_wide_matmul(
1598                                    params.b_off,
1599                                    params.n,
1600                                    &self.coop_f16_b_param,
1601                                    &self.coop_f16_vk_wide_b,
1602                                );
1603                            pass.set_bind_group(
1604                                0,
1605                                coop_f16_vk_bind_group(self, gpu_bi, qkv_coop_wide),
1606                                &[],
1607                            );
1608                            match kind {
1609                                MatmulQkvKind::CoopF16Vk => {
1610                                    if qkv_coop_wide {
1611                                        pass.set_pipeline(&matmul_qkv_kernel(&dev.device).pipeline);
1612                                        pass.dispatch_workgroups(
1613                                            params.n.div_ceil(32),
1614                                            m_s.div_ceil(32),
1615                                            1,
1616                                        );
1617                                    } else {
1618                                        let n_eff = scale(params.n);
1619                                        let mqk = matmul_qkv_coop_f16_vk_active_kernel(
1620                                            &dev.device,
1621                                            n_eff,
1622                                        )
1623                                        .expect("coop f16 matmul_qkv kernel missing");
1624                                        pass.set_pipeline(&mqk.pipeline);
1625                                        pass.dispatch_workgroups(
1626                                            m_s.div_ceil(16),
1627                                            params.n.div_ceil(16),
1628                                            1,
1629                                        );
1630                                    }
1631                                }
1632                                MatmulQkvKind::CoopF32 => {
1633                                    pass.set_pipeline(
1634                                        &matmul_qkv_coop_f32_kernel(&dev.device)
1635                                            .expect("coop matmul_qkv kernel missing")
1636                                            .pipeline,
1637                                    );
1638                                    pass.dispatch_workgroups(
1639                                        params.n.div_ceil(32),
1640                                        m_s.div_ceil(32),
1641                                        1,
1642                                    );
1643                                }
1644                                MatmulQkvKind::F32 => {
1645                                    pass.set_pipeline(&matmul_qkv_kernel(&dev.device).pipeline);
1646                                    pass.dispatch_workgroups(
1647                                        params.n.div_ceil(32),
1648                                        m_s.div_ceil(32),
1649                                        1,
1650                                    );
1651                                }
1652                            }
1653                        }
1654                        Step::GatherSplit { .. }
1655                        | Step::DequantMatmulGguf { .. }
1656                        | Step::DequantGroupedMatmulGguf { .. }
1657                        | Step::GatedDeltaNet { .. }
1658                        | Step::Lstm { .. }
1659                        | Step::ConvTranspose2d { .. }
1660                        | Step::GroupNormHost { .. }
1661                        | Step::LayerNorm2dHost { .. }
1662                        | Step::ResizeNearest2xHost { .. }
1663                        | Step::ReverseHost { .. }
1664                        | Step::ArgReduceHost { .. }
1665                        | Step::GruHost { .. }
1666                        | Step::RnnHost { .. }
1667                        | Step::Llada2GroupLimitedGate { .. }
1668                        | Step::UmapKnnHost { .. }
1669                        | Step::MsDeformAttnHost { .. }
1670                        | Step::FftHost { .. }
1671                        | Step::ScanHost { .. }
1672                        | Step::Im2ColHost { .. }
1673                        | Step::RngNormalHost { .. }
1674                        | Step::RngUniformHost { .. }
1675                        | Step::WelchPeaksHost { .. }
1676                        | Step::LogMelHost { .. }
1677                        | Step::LogMelBackwardHost { .. } => {}
1678                        #[cfg(feature = "splat")]
1679                        Step::GaussianSplatRender { .. }
1680                        | Step::GaussianSplatRenderBackward { .. }
1681                        | Step::GaussianSplatPrepare { .. }
1682                        | Step::GaussianSplatRasterize { .. } => {}
1683                    }
1684                    if !matches!(step, Step::FftGpu { .. }) {
1685                        gpu_bi += 1;
1686                    }
1687                    step_i += 1;
1688                    pass_dispatched = true;
1689                }
1690            }
1691            let needs_f16_drain = step_i < self.schedule.len()
1692                && !step_runs_on_host(&self.schedule[step_i])
1693                && step_i > 0
1694                && step_needs_pass_flush(&self.schedule[step_i], &self.schedule[step_i - 1]);
1695            let gpu_schedule_done = step_i >= self.schedule.len();
1696            let skip_readback = rlx_ir::env::flag("RLX_BENCH_DISPATCH_ONLY") || self.dispatch_only;
1697            let defer_tail = gpu_schedule_done && self.schedule.iter().any(step_is_tail_host);
1698            let mut fused_readback: Option<(
1699                ReadbackLayout,
1700                std::sync::mpsc::Receiver<Result<(), wgpu::BufferAsyncError>>,
1701                Vec<usize>,
1702            )> = None;
1703            if gpu_schedule_done && !skip_readback && !defer_tail {
1704                if !self.gpu_handle_feeds.is_empty() {
1705                    self.propagate_gpu_handle_feeds_on_gpu(dev, &mut enc);
1706                }
1707                let plan = self.readback_plan();
1708                let out_ids_all: Vec<_> = self.graph.outputs.clone();
1709                let out_ids: Vec<_> = plan.iter().map(|&i| out_ids_all[i]).collect();
1710                let layout = ReadbackLayout::for_nodes(&self.arena, &out_ids);
1711                if use_tiny_readback(&layout, out_ids.len()) && plan == vec![0] {
1712                    if self.tiny_readback.is_none() {
1713                        self.tiny_readback = Some(TinyReadbackStaging::new(&dev.device));
1714                    }
1715                    let tiny = self.tiny_readback.as_ref().expect("tiny readback");
1716                    encode_readback_copies(&mut enc, &self.arena, tiny.buffer(), &out_ids, &layout);
1717                    let map_rx = schedule_readback_map(&mut enc, tiny.buffer(), &layout);
1718                    let sub = dev.queue.submit(std::iter::once(enc.finish()));
1719                    wait_readback_map(&dev.device, sub, &map_rx, layout.total_bytes);
1720                    map_rx.recv().unwrap().unwrap();
1721                    return self.pack_readback_outputs(
1722                        &plan,
1723                        vec![decode_tiny_mapped_f32(tiny.buffer(), layout.total_bytes)],
1724                    );
1725                }
1726                ReadbackStaging::prepare(
1727                    &dev.device,
1728                    &mut self.readback_staging,
1729                    layout.total_bytes,
1730                );
1731                if let Some(staging) = self.readback_staging.as_ref() {
1732                    encode_readback_copies(
1733                        &mut enc,
1734                        &self.arena,
1735                        staging.buffer(),
1736                        &out_ids,
1737                        &layout,
1738                    );
1739                    let map_rx = schedule_readback_map(&mut enc, staging.buffer(), &layout);
1740                    fused_readback = Some((layout, map_rx, plan));
1741                }
1742            }
1743            let main_submission = dev.queue.submit(std::iter::once(enc.finish()));
1744            if defer_tail {
1745                let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1746                self.run_tail_host_audio_ops(dev);
1747                if !skip_readback {
1748                    let mut rb_enc =
1749                        dev.device
1750                            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1751                                label: Some("rlx-wgpu readback after tail-host"),
1752                            });
1753                    if !self.gpu_handle_feeds.is_empty() {
1754                        self.propagate_gpu_handle_feeds_on_gpu(dev, &mut rb_enc);
1755                    }
1756                    let plan = self.readback_plan();
1757                    let out_ids_all: Vec<_> = self.graph.outputs.clone();
1758                    let out_ids: Vec<_> = plan.iter().map(|&i| out_ids_all[i]).collect();
1759                    let layout = ReadbackLayout::for_nodes(&self.arena, &out_ids);
1760                    if use_tiny_readback(&layout, out_ids.len()) && plan == vec![0] {
1761                        if self.tiny_readback.is_none() {
1762                            self.tiny_readback = Some(TinyReadbackStaging::new(&dev.device));
1763                        }
1764                        let tiny = self.tiny_readback.as_ref().expect("tiny readback");
1765                        encode_readback_copies(
1766                            &mut rb_enc,
1767                            &self.arena,
1768                            tiny.buffer(),
1769                            &out_ids,
1770                            &layout,
1771                        );
1772                        let map_rx = schedule_readback_map(&mut rb_enc, tiny.buffer(), &layout);
1773                        let sub = dev.queue.submit(std::iter::once(rb_enc.finish()));
1774                        wait_readback_map(&dev.device, sub, &map_rx, layout.total_bytes);
1775                        map_rx.recv().unwrap().unwrap();
1776                        return self.pack_readback_outputs(
1777                            &plan,
1778                            vec![decode_tiny_mapped_f32(tiny.buffer(), layout.total_bytes)],
1779                        );
1780                    }
1781                    ReadbackStaging::prepare(
1782                        &dev.device,
1783                        &mut self.readback_staging,
1784                        layout.total_bytes,
1785                    );
1786                    if let Some(staging) = self.readback_staging.as_ref() {
1787                        encode_readback_copies(
1788                            &mut rb_enc,
1789                            &self.arena,
1790                            staging.buffer(),
1791                            &out_ids,
1792                            &layout,
1793                        );
1794                        let map_rx = schedule_readback_map(&mut rb_enc, staging.buffer(), &layout);
1795                        let sub = dev.queue.submit(std::iter::once(rb_enc.finish()));
1796                        wait_readback_map(&dev.device, sub, &map_rx, layout.total_bytes);
1797                        map_rx.recv().unwrap().unwrap();
1798                        self.dump_node_stats_if_requested(dev);
1799                        let partial = decode_mapped_readback_f32(staging.buffer(), &layout);
1800                        return self.pack_readback_outputs(&plan, partial);
1801                    }
1802                }
1803            }
1804            if needs_f16_drain {
1805                let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1806            }
1807            let need_host_sync =
1808                step_i < self.schedule.len() && step_runs_on_host(&self.schedule[step_i]);
1809            if need_host_sync {
1810                let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1811            }
1812            if gpu_schedule_done {
1813                if skip_readback || defer_tail {
1814                    return self
1815                        .graph
1816                        .outputs
1817                        .iter()
1818                        .map(|&id| {
1819                            let n = self.graph.node(id).shape.num_elements().unwrap_or(0);
1820                            vec![0.0; n]
1821                        })
1822                        .collect();
1823                }
1824                if let (Some((layout, map_rx, plan)), Some(staging)) =
1825                    (fused_readback, self.readback_staging.as_ref())
1826                {
1827                    wait_readback_map(&dev.device, main_submission, &map_rx, layout.total_bytes);
1828                    map_rx.recv().unwrap().unwrap();
1829                    self.dump_node_stats_if_requested(dev);
1830                    let partial = decode_mapped_readback_f32(staging.buffer(), &layout);
1831                    return self.pack_readback_outputs(&plan, partial);
1832                }
1833                break;
1834            }
1835            match &self.schedule[step_i] {
1836                Step::BufferCopy {
1837                    src_byte_off,
1838                    dst_byte_off,
1839                    bytes,
1840                } => {
1841                    // wgpu forbids `copy_buffer_to_buffer` on the same buffer;
1842                    // use the generic copy compute kernel instead.
1843                    let src = *src_byte_off;
1844                    let dst = *dst_byte_off;
1845                    let nbytes = *bytes as u64;
1846                    let elems = (nbytes / 4).max(1) as u32;
1847                    let lo = src.min(dst);
1848                    let hi = src.saturating_add(nbytes).max(dst.saturating_add(nbytes));
1849                    let max_binding = dev.device.limits().max_storage_buffer_binding_size;
1850                    // On >4 GiB arenas (GGUF decode) src and dst can be more than
1851                    // `max_binding` apart, so no single ≤4 GiB compute bind window
1852                    // covers both — the kernel copy would silently read/write out
1853                    // of its window. Fall back to a host round-trip (small staging
1854                    // copies only; capped by ARENA_STAGE_CAP).
1855                    if hi.saturating_sub(lo) > max_binding {
1856                        let bytes_host = self.arena.read_bytes_range(
1857                            &dev.device,
1858                            &dev.queue,
1859                            src as usize,
1860                            nbytes as usize,
1861                        );
1862                        self.arena
1863                            .write_bytes_range(&dev.queue, dst as usize, &bytes_host);
1864                        let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1865                        step_i += 1;
1866                        continue;
1867                    }
1868                    let mut base = (lo / 256) * 256;
1869                    // The bind window must cover [base, hi]. `base` is floored to 256B
1870                    // alignment (so base ≤ lo); measuring the size from `lo` instead of
1871                    // `base` clips the window when the copy straddles a 256B boundary,
1872                    // silently dropping the tail (e.g. a 92B Bool→F32 mask copy whose
1873                    // dst sat just past the window → x_mask stayed 0).
1874                    let span = hi.saturating_sub(base).max(1);
1875                    let mut size = span.div_ceil(256) * 256;
1876                    size = size.max(256).min(max_binding);
1877                    if base.saturating_add(size) > self.arena.size as u64 {
1878                        base = (self.arena.size as u64).saturating_sub(size);
1879                        base = (base / 256) * 256;
1880                    }
1881                    let p = CopyParams {
1882                        n: elems,
1883                        in_off: (src.saturating_sub(base) / 4) as u32,
1884                        out_off: (dst.saturating_sub(base) / 4) as u32,
1885                        _p0: 0,
1886                        _p1: 0,
1887                        _p2: 0,
1888                        _p3: 0,
1889                        _p4: 0,
1890                    };
1891                    let ck = copy_kernel(&dev.device);
1892                    let u = dev.device.create_buffer(&wgpu::BufferDescriptor {
1893                        label: Some("rlx-wgpu arena_copy uniform"),
1894                        size: std::mem::size_of::<CopyParams>() as u64,
1895                        usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1896                        mapped_at_creation: false,
1897                    });
1898                    dev.queue.write_buffer(&u, 0, bytemuck::bytes_of(&p));
1899                    let bg =
1900                        bind_two_buf0_window(&dev.device, ck, &self.arena.buffer, base, size, &u);
1901                    let mut enc =
1902                        dev.device
1903                            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1904                                label: Some("rlx-wgpu arena_copy"),
1905                            });
1906                    {
1907                        let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
1908                            label: Some("rlx-wgpu arena_copy pass"),
1909                            ..Default::default()
1910                        });
1911                        pass.set_pipeline(&ck.pipeline);
1912                        pass.set_bind_group(0, &bg, &[]);
1913                        let (gx, gy, gz) = dispatch_dims(elems, 64);
1914                        pass.dispatch_workgroups(gx, gy, gz);
1915                    }
1916                    dev.queue.submit(std::iter::once(enc.finish()));
1917                    let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1918                }
1919                Step::GatherSplit {
1920                    n_out,
1921                    n_idx,
1922                    dim,
1923                    vocab,
1924                    table_byte_off,
1925                    idx_byte_off,
1926                    out_byte_off,
1927                } => {
1928                    run_gather_split(
1929                        &self.arena,
1930                        &dev.device,
1931                        &dev.queue,
1932                        *n_out,
1933                        *n_idx,
1934                        *dim,
1935                        *vocab,
1936                        *table_byte_off as usize,
1937                        *idx_byte_off as usize,
1938                        *out_byte_off as usize,
1939                    );
1940                }
1941                Step::DequantMatmulGguf {
1942                    m,
1943                    k,
1944                    n,
1945                    scheme_id,
1946                    x_byte_off,
1947                    w_byte_off,
1948                    out_byte_off,
1949                } => {
1950                    if *m == 1 && crate::gguf_gpu::gemv_supports_scheme(*scheme_id) {
1951                        // Decode GEMV: fused dequant+matmul, windowed bindings,
1952                        // no f32 scratch (handles arenas larger than the 4 GiB
1953                        // single-binding limit).
1954                        crate::gguf_gpu::run_dequant_matmul_gguf_gemv(
1955                            &self.arena,
1956                            &dev.device,
1957                            &dev.queue,
1958                            *k as usize,
1959                            *n as usize,
1960                            *scheme_id,
1961                            *x_byte_off as usize,
1962                            *w_byte_off as usize,
1963                            *out_byte_off as usize,
1964                        );
1965                    } else if self.dequant_scratch_off > 0 {
1966                        crate::gguf_gpu::run_dequant_matmul_gguf_gpu(
1967                            &self.arena,
1968                            &dev.device,
1969                            &dev.queue,
1970                            *m as usize,
1971                            *k as usize,
1972                            *n as usize,
1973                            *scheme_id,
1974                            *x_byte_off as usize,
1975                            *w_byte_off as usize,
1976                            self.dequant_scratch_off,
1977                            *out_byte_off as usize,
1978                        );
1979                    } else {
1980                        crate::gguf_host::run_dequant_matmul_gguf(
1981                            &self.arena,
1982                            &dev.device,
1983                            &dev.queue,
1984                            *m as usize,
1985                            *k as usize,
1986                            *n as usize,
1987                            *scheme_id,
1988                            *x_byte_off as usize,
1989                            *w_byte_off as usize,
1990                            *out_byte_off as usize,
1991                        );
1992                    }
1993                }
1994                Step::DequantGroupedMatmulGguf {
1995                    m,
1996                    k,
1997                    n,
1998                    num_experts,
1999                    scheme_id,
2000                    x_byte_off,
2001                    w_byte_off,
2002                    idx_byte_off,
2003                    out_byte_off,
2004                } => {
2005                    if self.dequant_scratch_off > 0 {
2006                        crate::gguf_gpu::run_dequant_grouped_matmul_gguf_gpu(
2007                            &self.arena,
2008                            &dev.device,
2009                            &dev.queue,
2010                            *m as usize,
2011                            *k as usize,
2012                            *n as usize,
2013                            *num_experts as usize,
2014                            *scheme_id,
2015                            *x_byte_off as usize,
2016                            *w_byte_off as usize,
2017                            *idx_byte_off as usize,
2018                            self.dequant_scratch_off,
2019                            *out_byte_off as usize,
2020                        );
2021                    } else {
2022                        crate::gguf_host::run_dequant_grouped_matmul_gguf(
2023                            &self.arena,
2024                            &dev.device,
2025                            &dev.queue,
2026                            *m as usize,
2027                            *k as usize,
2028                            *n as usize,
2029                            *num_experts as usize,
2030                            *scheme_id,
2031                            *x_byte_off as usize,
2032                            *w_byte_off as usize,
2033                            *idx_byte_off as usize,
2034                            *out_byte_off as usize,
2035                        );
2036                    }
2037                }
2038                Step::GatedDeltaNet {
2039                    q_byte_off,
2040                    k_byte_off,
2041                    v_byte_off,
2042                    g_byte_off,
2043                    beta_byte_off,
2044                    state_byte_off,
2045                    dst_byte_off,
2046                    batch,
2047                    seq,
2048                    heads,
2049                    state_size,
2050                    use_carry,
2051                } => {
2052                    crate::gdn_host::run_gated_delta_net(
2053                        &self.arena,
2054                        &dev.device,
2055                        &dev.queue,
2056                        *q_byte_off as usize,
2057                        *k_byte_off as usize,
2058                        *v_byte_off as usize,
2059                        *g_byte_off as usize,
2060                        *beta_byte_off as usize,
2061                        *state_byte_off as usize,
2062                        *dst_byte_off as usize,
2063                        *batch as usize,
2064                        *seq as usize,
2065                        *heads as usize,
2066                        *state_size as usize,
2067                        *use_carry,
2068                    );
2069                }
2070                Step::Lstm {
2071                    x_byte_off,
2072                    w_ih_byte_off,
2073                    w_hh_byte_off,
2074                    bias_byte_off,
2075                    h0_byte_off,
2076                    c0_byte_off,
2077                    dst_byte_off,
2078                    batch,
2079                    seq,
2080                    input_size,
2081                    hidden,
2082                    num_layers,
2083                    bidirectional,
2084                    carry,
2085                } => {
2086                    crate::lstm_host::run_lstm(
2087                        &self.arena,
2088                        &dev.device,
2089                        &dev.queue,
2090                        *x_byte_off as usize,
2091                        *w_ih_byte_off as usize,
2092                        *w_hh_byte_off as usize,
2093                        *bias_byte_off as usize,
2094                        *h0_byte_off as usize,
2095                        *c0_byte_off as usize,
2096                        *dst_byte_off as usize,
2097                        *batch as usize,
2098                        *seq as usize,
2099                        *input_size as usize,
2100                        *hidden as usize,
2101                        *num_layers as usize,
2102                        *bidirectional,
2103                        *carry,
2104                    );
2105                }
2106                Step::ConvTranspose2d {
2107                    src_byte_off,
2108                    weight_byte_off,
2109                    dst_byte_off,
2110                    n,
2111                    c_in,
2112                    h,
2113                    w_in,
2114                    c_out,
2115                    h_out,
2116                    w_out,
2117                    kh,
2118                    kw,
2119                    sh,
2120                    sw,
2121                    ph,
2122                    pw,
2123                    dh,
2124                    dw,
2125                    groups,
2126                } => {
2127                    crate::conv_transpose2d_host::run_conv_transpose2d(
2128                        &self.arena,
2129                        &dev.device,
2130                        &dev.queue,
2131                        *src_byte_off as usize,
2132                        *weight_byte_off as usize,
2133                        *dst_byte_off as usize,
2134                        *n as usize,
2135                        *c_in as usize,
2136                        *h as usize,
2137                        *w_in as usize,
2138                        *c_out as usize,
2139                        *h_out as usize,
2140                        *w_out as usize,
2141                        *kh as usize,
2142                        *kw as usize,
2143                        *sh as usize,
2144                        *sw as usize,
2145                        *ph as usize,
2146                        *pw as usize,
2147                        *dh as usize,
2148                        *dw as usize,
2149                        *groups as usize,
2150                    );
2151                }
2152                Step::GroupNormHost {
2153                    src_byte_off,
2154                    gamma_byte_off,
2155                    beta_byte_off,
2156                    dst_byte_off,
2157                    n,
2158                    c,
2159                    h,
2160                    w,
2161                    num_groups,
2162                    eps,
2163                } => {
2164                    crate::vision_host::run_group_norm(
2165                        &self.arena,
2166                        &dev.device,
2167                        &dev.queue,
2168                        *src_byte_off as usize,
2169                        *gamma_byte_off as usize,
2170                        *beta_byte_off as usize,
2171                        *dst_byte_off as usize,
2172                        *n as usize,
2173                        *c as usize,
2174                        *h as usize,
2175                        *w as usize,
2176                        *num_groups as usize,
2177                        *eps,
2178                    );
2179                }
2180                Step::LayerNorm2dHost {
2181                    src_byte_off,
2182                    gamma_byte_off,
2183                    beta_byte_off,
2184                    dst_byte_off,
2185                    n,
2186                    c,
2187                    h,
2188                    w,
2189                    eps,
2190                } => {
2191                    crate::vision_host::run_layer_norm2d(
2192                        &self.arena,
2193                        &dev.device,
2194                        &dev.queue,
2195                        *src_byte_off as usize,
2196                        *gamma_byte_off as usize,
2197                        *beta_byte_off as usize,
2198                        *dst_byte_off as usize,
2199                        *n as usize,
2200                        *c as usize,
2201                        *h as usize,
2202                        *w as usize,
2203                        *eps,
2204                    );
2205                }
2206                Step::ResizeNearest2xHost {
2207                    src_byte_off,
2208                    dst_byte_off,
2209                    n,
2210                    c,
2211                    h,
2212                    w,
2213                } => {
2214                    crate::vision_host::run_resize_nearest_2x(
2215                        &self.arena,
2216                        &dev.device,
2217                        &dev.queue,
2218                        *src_byte_off as usize,
2219                        *dst_byte_off as usize,
2220                        *n as usize,
2221                        *c as usize,
2222                        *h as usize,
2223                        *w as usize,
2224                    );
2225                }
2226                Step::ReverseHost {
2227                    src_byte_off,
2228                    dst_byte_off,
2229                    dims,
2230                    rev_mask,
2231                    elem_bytes,
2232                } => {
2233                    crate::vision_host::run_reverse(
2234                        &self.arena,
2235                        &dev.device,
2236                        &dev.queue,
2237                        *src_byte_off as usize,
2238                        *dst_byte_off as usize,
2239                        dims,
2240                        rev_mask,
2241                        *elem_bytes as usize,
2242                    );
2243                }
2244                Step::ArgReduceHost {
2245                    src_byte_off,
2246                    dst_byte_off,
2247                    outer,
2248                    reduced,
2249                    inner,
2250                    is_max,
2251                } => {
2252                    crate::vision_host::run_argreduce(
2253                        &self.arena,
2254                        &dev.device,
2255                        &dev.queue,
2256                        *src_byte_off as usize,
2257                        *dst_byte_off as usize,
2258                        *outer as usize,
2259                        *reduced as usize,
2260                        *inner as usize,
2261                        *is_max,
2262                    );
2263                }
2264                Step::GruHost {
2265                    x,
2266                    w_ih,
2267                    w_hh,
2268                    b_ih,
2269                    b_hh,
2270                    h0,
2271                    dst,
2272                    batch,
2273                    seq,
2274                    input_size,
2275                    hidden,
2276                    num_layers,
2277                    bidirectional,
2278                    carry,
2279                } => {
2280                    crate::vision_host::run_gru(
2281                        &self.arena,
2282                        &dev.device,
2283                        &dev.queue,
2284                        *x as usize,
2285                        *w_ih as usize,
2286                        *w_hh as usize,
2287                        *b_ih as usize,
2288                        *b_hh as usize,
2289                        *h0 as usize,
2290                        *dst as usize,
2291                        *batch as usize,
2292                        *seq as usize,
2293                        *input_size as usize,
2294                        *hidden as usize,
2295                        *num_layers as usize,
2296                        *bidirectional,
2297                        *carry,
2298                    );
2299                }
2300                Step::RnnHost {
2301                    x,
2302                    w_ih,
2303                    w_hh,
2304                    bias,
2305                    h0,
2306                    dst,
2307                    batch,
2308                    seq,
2309                    input_size,
2310                    hidden,
2311                    num_layers,
2312                    bidirectional,
2313                    carry,
2314                    relu,
2315                } => {
2316                    crate::vision_host::run_rnn(
2317                        &self.arena,
2318                        &dev.device,
2319                        &dev.queue,
2320                        *x as usize,
2321                        *w_ih as usize,
2322                        *w_hh as usize,
2323                        *bias as usize,
2324                        *h0 as usize,
2325                        *dst as usize,
2326                        *batch as usize,
2327                        *seq as usize,
2328                        *input_size as usize,
2329                        *hidden as usize,
2330                        *num_layers as usize,
2331                        *bidirectional,
2332                        *carry,
2333                        *relu,
2334                    );
2335                }
2336                Step::Llada2GroupLimitedGate {
2337                    sig_byte_off,
2338                    route_byte_off,
2339                    out_byte_off,
2340                    n_elems,
2341                    attrs,
2342                } => {
2343                    crate::llada2_gate_host::run_llada2_group_limited_gate(
2344                        &self.arena,
2345                        &dev.device,
2346                        &dev.queue,
2347                        *sig_byte_off as usize,
2348                        *route_byte_off as usize,
2349                        *out_byte_off as usize,
2350                        *n_elems as usize,
2351                        attrs,
2352                    );
2353                }
2354                Step::UmapKnnHost {
2355                    pairwise_byte_off,
2356                    out_byte_off,
2357                    n,
2358                    k,
2359                } => {
2360                    crate::umap_knn_host::run_umap_knn(
2361                        &self.arena,
2362                        &dev.device,
2363                        &dev.queue,
2364                        *pairwise_byte_off as usize,
2365                        *out_byte_off as usize,
2366                        *n as usize,
2367                        *k as usize,
2368                    );
2369                }
2370                Step::MsDeformAttnHost {
2371                    in_offs,
2372                    out_byte_off,
2373                    out_bytes,
2374                    attrs,
2375                } => {
2376                    crate::ms_deform_attn::run_ms_deform_attn(
2377                        &self.arena,
2378                        &dev.device,
2379                        &dev.queue,
2380                        in_offs,
2381                        *out_byte_off as usize,
2382                        *out_bytes as usize,
2383                        attrs,
2384                    );
2385                }
2386                Step::FftHost {
2387                    src_byte_off,
2388                    dst_byte_off,
2389                    outer,
2390                    n_complex,
2391                    inverse,
2392                    norm_tag,
2393                    dtype_tag,
2394                } => {
2395                    crate::fft_host::run_fft1d(
2396                        &self.arena,
2397                        &dev.device,
2398                        &dev.queue,
2399                        *src_byte_off as usize,
2400                        *dst_byte_off as usize,
2401                        *outer as usize,
2402                        *n_complex as usize,
2403                        *inverse,
2404                        *norm_tag,
2405                        fft_dtype_from_tag(*dtype_tag),
2406                    );
2407                }
2408                Step::ScanHost {
2409                    plan,
2410                    outer_init_off,
2411                    outer_final_off,
2412                    length,
2413                    save_trajectory,
2414                    xs_outer,
2415                    bcast_outer,
2416                } => {
2417                    crate::scan_host::run_scan(
2418                        &self.arena,
2419                        &dev.device,
2420                        &dev.queue,
2421                        plan,
2422                        *outer_init_off,
2423                        *outer_final_off,
2424                        *length,
2425                        *save_trajectory,
2426                        xs_outer,
2427                        bcast_outer,
2428                    );
2429                }
2430                Step::WelchPeaksHost { .. }
2431                | Step::LogMelHost { .. }
2432                | Step::LogMelBackwardHost { .. } => {
2433                    unreachable!("tail-host audio ops run after GPU wait")
2434                }
2435                Step::Im2ColHost {
2436                    x_byte_off,
2437                    col_byte_off,
2438                    n,
2439                    c_in,
2440                    h,
2441                    w,
2442                    h_out,
2443                    w_out,
2444                    kh,
2445                    kw,
2446                    sh,
2447                    sw,
2448                    ph,
2449                    pw,
2450                    dh,
2451                    dw_dil,
2452                } => {
2453                    crate::im2col_host::run_im2col(
2454                        &self.arena,
2455                        &dev.device,
2456                        &dev.queue,
2457                        *x_byte_off as usize,
2458                        *col_byte_off as usize,
2459                        *n,
2460                        *c_in,
2461                        *h,
2462                        *w,
2463                        *h_out,
2464                        *w_out,
2465                        *kh,
2466                        *kw,
2467                        *sh,
2468                        *sw,
2469                        *ph,
2470                        *pw,
2471                        *dh,
2472                        *dw_dil,
2473                    );
2474                }
2475                Step::RngNormalHost {
2476                    dst_byte_off,
2477                    len,
2478                    mean,
2479                    scale,
2480                    key,
2481                    op_seed,
2482                } => {
2483                    let opts = *self.rng.read().expect("rng lock");
2484                    crate::rng_host::run_rng_normal(
2485                        &self.arena,
2486                        &dev.queue,
2487                        *dst_byte_off as usize,
2488                        *len as usize,
2489                        *mean,
2490                        *scale,
2491                        *key,
2492                        *op_seed,
2493                        opts,
2494                    );
2495                }
2496                Step::RngUniformHost {
2497                    dst_byte_off,
2498                    len,
2499                    low,
2500                    high,
2501                    key,
2502                    op_seed,
2503                } => {
2504                    let opts = *self.rng.read().expect("rng lock");
2505                    crate::rng_host::run_rng_uniform(
2506                        &self.arena,
2507                        &dev.queue,
2508                        *dst_byte_off as usize,
2509                        *len as usize,
2510                        *low,
2511                        *high,
2512                        *key,
2513                        *op_seed,
2514                        opts,
2515                    );
2516                }
2517                #[cfg(feature = "splat")]
2518                Step::GaussianSplatRender {
2519                    positions_byte_off,
2520                    positions_len,
2521                    scales_byte_off,
2522                    scales_len,
2523                    rotations_byte_off,
2524                    rotations_len,
2525                    opacities_byte_off,
2526                    opacities_len,
2527                    colors_byte_off,
2528                    colors_len,
2529                    sh_coeffs_byte_off,
2530                    sh_coeffs_len,
2531                    meta_byte_off,
2532                    dst_byte_off,
2533                    dst_len,
2534                    width,
2535                    height,
2536                    tile_size,
2537                    radius_scale,
2538                    alpha_cutoff,
2539                    max_splat_steps,
2540                    transmittance_threshold,
2541                    max_list_entries,
2542                } => {
2543                    crate::splat::run_gaussian_splat_render(
2544                        &self.arena,
2545                        &dev.device,
2546                        &dev.queue,
2547                        *positions_byte_off as usize,
2548                        *positions_len as usize,
2549                        *scales_byte_off as usize,
2550                        *scales_len as usize,
2551                        *rotations_byte_off as usize,
2552                        *rotations_len as usize,
2553                        *opacities_byte_off as usize,
2554                        *opacities_len as usize,
2555                        *colors_byte_off as usize,
2556                        *colors_len as usize,
2557                        *sh_coeffs_byte_off as usize,
2558                        *sh_coeffs_len as usize,
2559                        *meta_byte_off as usize,
2560                        *dst_byte_off as usize,
2561                        *dst_len as usize,
2562                        *width,
2563                        *height,
2564                        *tile_size,
2565                        *radius_scale,
2566                        *alpha_cutoff,
2567                        *max_splat_steps,
2568                        *transmittance_threshold,
2569                        *max_list_entries,
2570                    );
2571                }
2572                #[cfg(feature = "splat")]
2573                Step::GaussianSplatPrepare {
2574                    positions_byte_off,
2575                    positions_len,
2576                    scales_byte_off,
2577                    scales_len,
2578                    rotations_byte_off,
2579                    rotations_len,
2580                    opacities_byte_off,
2581                    opacities_len,
2582                    colors_byte_off,
2583                    colors_len,
2584                    sh_coeffs_byte_off,
2585                    sh_coeffs_len,
2586                    meta_byte_off,
2587                    meta_len,
2588                    prep_byte_off,
2589                    prep_len,
2590                    width,
2591                    height,
2592                    tile_size,
2593                    radius_scale,
2594                    alpha_cutoff,
2595                    max_splat_steps,
2596                    transmittance_threshold,
2597                    max_list_entries,
2598                } => {
2599                    crate::splat::run_gaussian_splat_prepare(
2600                        &self.arena,
2601                        &dev.device,
2602                        &dev.queue,
2603                        *positions_byte_off as usize,
2604                        *positions_len as usize,
2605                        *scales_byte_off as usize,
2606                        *scales_len as usize,
2607                        *rotations_byte_off as usize,
2608                        *rotations_len as usize,
2609                        *opacities_byte_off as usize,
2610                        *opacities_len as usize,
2611                        *colors_byte_off as usize,
2612                        *colors_len as usize,
2613                        *sh_coeffs_byte_off as usize,
2614                        *sh_coeffs_len as usize,
2615                        *meta_byte_off as usize,
2616                        *meta_len as usize,
2617                        *prep_byte_off as usize,
2618                        *prep_len as usize,
2619                        *width,
2620                        *height,
2621                        *tile_size,
2622                        *radius_scale,
2623                        *alpha_cutoff,
2624                        *max_splat_steps,
2625                        *transmittance_threshold,
2626                        *max_list_entries,
2627                    );
2628                }
2629                #[cfg(feature = "splat")]
2630                Step::GaussianSplatRasterize {
2631                    prep_byte_off,
2632                    prep_len,
2633                    meta_byte_off,
2634                    meta_len,
2635                    dst_byte_off,
2636                    dst_len,
2637                    count,
2638                    width,
2639                    height,
2640                    tile_size,
2641                    alpha_cutoff,
2642                    max_splat_steps,
2643                    transmittance_threshold,
2644                    max_list_entries,
2645                } => {
2646                    crate::splat::run_gaussian_splat_rasterize(
2647                        &self.arena,
2648                        &dev.device,
2649                        &dev.queue,
2650                        *prep_byte_off as usize,
2651                        *prep_len as usize,
2652                        *meta_byte_off as usize,
2653                        *meta_len as usize,
2654                        *dst_byte_off as usize,
2655                        *dst_len as usize,
2656                        *count as usize,
2657                        *width,
2658                        *height,
2659                        *tile_size,
2660                        *alpha_cutoff,
2661                        *max_splat_steps,
2662                        *transmittance_threshold,
2663                        *max_list_entries,
2664                    );
2665                }
2666                #[cfg(feature = "splat")]
2667                Step::GaussianSplatRenderBackward {
2668                    positions_byte_off,
2669                    positions_len,
2670                    scales_byte_off,
2671                    scales_len,
2672                    rotations_byte_off,
2673                    rotations_len,
2674                    opacities_byte_off,
2675                    opacities_len,
2676                    colors_byte_off,
2677                    colors_len,
2678                    sh_coeffs_byte_off,
2679                    sh_coeffs_len,
2680                    meta_byte_off,
2681                    d_loss_byte_off,
2682                    d_loss_len,
2683                    packed_byte_off,
2684                    packed_len,
2685                    width,
2686                    height,
2687                    tile_size,
2688                    radius_scale,
2689                    alpha_cutoff,
2690                    max_splat_steps,
2691                    transmittance_threshold,
2692                    max_list_entries,
2693                    loss_grad_clip,
2694                    sh_band,
2695                    max_anisotropy,
2696                } => {
2697                    crate::splat::run_gaussian_splat_render_backward(
2698                        &self.arena,
2699                        &dev.device,
2700                        &dev.queue,
2701                        *positions_byte_off as usize,
2702                        *positions_len as usize,
2703                        *scales_byte_off as usize,
2704                        *scales_len as usize,
2705                        *rotations_byte_off as usize,
2706                        *rotations_len as usize,
2707                        *opacities_byte_off as usize,
2708                        *opacities_len as usize,
2709                        *colors_byte_off as usize,
2710                        *colors_len as usize,
2711                        *sh_coeffs_byte_off as usize,
2712                        *sh_coeffs_len as usize,
2713                        *meta_byte_off as usize,
2714                        *d_loss_byte_off as usize,
2715                        *d_loss_len as usize,
2716                        *packed_byte_off as usize,
2717                        *packed_len as usize,
2718                        *width,
2719                        *height,
2720                        *tile_size,
2721                        *radius_scale,
2722                        *alpha_cutoff,
2723                        *max_splat_steps,
2724                        *transmittance_threshold,
2725                        *max_list_entries,
2726                        *loss_grad_clip,
2727                        *sh_band,
2728                        *max_anisotropy,
2729                    );
2730                }
2731                _ => break,
2732            }
2733            step_i += 1;
2734        }
2735
2736        self.dump_node_stats_if_requested(dev);
2737
2738        if rlx_ir::env::flag("RLX_WGPU_NAN_TRACE") {
2739            let mut bad_nodes = Vec::new();
2740            for node in self.graph.nodes() {
2741                if !self.arena.has(node.id) {
2742                    continue;
2743                }
2744                // Skip leaves — populated by host writes, not kernels.
2745                if matches!(
2746                    node.op,
2747                    rlx_ir::Op::Input { .. }
2748                        | rlx_ir::Op::Param { .. }
2749                        | rlx_ir::Op::Constant { .. }
2750                ) {
2751                    continue;
2752                }
2753                let data = self.arena.read_f32(&dev.device, &dev.queue, node.id);
2754                let nan_count = data.iter().filter(|v| v.is_nan()).count();
2755                let inf_count = data.iter().filter(|v| v.is_infinite()).count();
2756                if nan_count > 0 || inf_count > 0 {
2757                    // Capture first NaN index + the values around it.
2758                    let first_nan = data.iter().position(|v| v.is_nan());
2759                    if let Some(idx) = first_nan {
2760                        let lo = idx.saturating_sub(2);
2761                        let hi = (idx + 3).min(data.len());
2762                        eprintln!(
2763                            "  node {:?} op={:?} len={} nan={} inf={} \
2764                                   first_nan_idx={} ctx={:?}",
2765                            node.id,
2766                            node.op,
2767                            data.len(),
2768                            nan_count,
2769                            inf_count,
2770                            idx,
2771                            &data[lo..hi]
2772                        );
2773                    }
2774                    bad_nodes.push((node.id, data.len(), nan_count, inf_count));
2775                    if bad_nodes.len() >= 3 {
2776                        break;
2777                    }
2778                }
2779            }
2780            if bad_nodes.is_empty() {
2781                eprintln!("[wgpu-nan-trace] no NaN/Inf in any node — clean run");
2782            } else {
2783                eprintln!(
2784                    "[wgpu-nan-trace] first {} bad nodes (above)",
2785                    bad_nodes.len()
2786                );
2787            }
2788        }
2789
2790        if rlx_ir::env::flag("RLX_BENCH_DISPATCH_ONLY") {
2791            return self
2792                .graph
2793                .outputs
2794                .iter()
2795                .map(|&id| {
2796                    let n = self.graph.node(id).shape.num_elements().unwrap_or(0);
2797                    vec![0.0; n]
2798                })
2799                .collect();
2800        }
2801        let out_ids: Vec<_> = self.graph.outputs.clone();
2802        read_f32_many_pooled(
2803            &self.arena,
2804            &dev.device,
2805            &dev.queue,
2806            &out_ids,
2807            &mut self.readback_staging,
2808        )
2809    }
2810}