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, conv2d_kernel,
38    conv3d_kernel, copy_kernel, cumsum_backward_kernel, cumsum_kernel, dequant_matmul_kernel,
39    elementwise_region_kernel, elementwise_region_spatial_kernel, expand_kernel, fma_kernel,
40    fused_residual_ln_kernel, fused_residual_ln_tee_kernel, fused_residual_rms_norm_kernel,
41    gather_axis_kernel, gather_backward_acc_kernel, gather_backward_zero_kernel, gather_kernel,
42    gather_split_kernel, grouped_matmul_kernel, gru_kernel,
43    layer_norm_backward_gamma_partial_kernel, layer_norm_backward_gamma_reduce_kernel,
44    layer_norm_backward_input_kernel, layernorm_kernel, mamba2_kernel,
45    matmul_coop_f16_vulkan_active_kernel, matmul_coop_f16_vulkan_kernel,
46    matmul_coop_f32_active_kernel, matmul_coop16_kernel, matmul_f16_compute_kernel,
47    matmul_f16w_kernel, matmul_kernel, matmul_qkv_coop_f16_vk_active_kernel,
48    matmul_qkv_coop_f16_vk_kernel, matmul_qkv_coop_f32_kernel, matmul_qkv_kernel,
49    matmul_wide_active_kernel, matmul_wide_kernel, narrow_kernel, pool1d_kernel, pool2d_kernel,
50    pool3d_kernel, reduce_kernel, rms_norm_backward_kernel, rms_norm_backward_param_kernel,
51    rnn_kernel, rope_backward_kernel, rope_kernel, sample_kernel, scatter_add_kernel,
52    selective_scan_kernel, softmax_cross_entropy_kernel, softmax_kernel, topk_kernel,
53    transpose_kernel, umap_knn_kernel, unary_f16_mirror_kernel, unary_kernel,
54    welch_peaks_gpu_kernel, where_kernel,
55};
56use rlx_ir::dynamic::{bind_graph, has_dynamic_dims, infer_bindings_from_f32_inputs, same_binding};
57use rlx_ir::op::{Activation, BinaryOp, CmpOp, MaskKind, ReduceOp};
58use rlx_ir::shape::DimBinding;
59use rlx_ir::{Graph, NodeId, Op};
60use std::collections::{HashMap, HashSet};
61use std::num::NonZeroU64;
62
63use super::*;
64
65impl WgpuExecutable {
66    pub fn 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 } => {
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                    Step::GatherSplit { .. }
664                    | Step::DequantMatmulGguf { .. }
665                    | Step::DequantGroupedMatmulGguf { .. }
666                    | Step::GatedDeltaNet { .. }
667                    | Step::Lstm { .. }
668                    | Step::ConvTranspose2d { .. }
669                    | Step::GroupNormHost { .. }
670                    | Step::LayerNorm2dHost { .. }
671                    | Step::ResizeNearest2xHost { .. }
672                    | Step::ReverseHost { .. }
673                    | Step::ArgReduceHost { .. }
674                    | Step::GruHost { .. }
675                    | Step::RnnHost { .. }
676                    | Step::Llada2GroupLimitedGate { .. }
677                    | Step::UmapKnnHost { .. }
678                    | Step::MsDeformAttnHost { .. }
679                    | Step::FftHost { .. }
680                    | Step::ScanHost { .. }
681                    | Step::Im2ColHost { .. }
682                    | Step::RngNormalHost { .. }
683                    | Step::RngUniformHost { .. }
684                    | Step::WelchPeaksHost { .. }
685                    | Step::LogMelHost { .. }
686                    | Step::LogMelBackwardHost { .. } => {}
687                    Step::FusedResidualLn { params } => {
688                        let mut p = *params;
689                        p.outer = scale(p.outer);
690                        dev.queue
691                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
692                    }
693                    Step::FusedResidualLnTee { params } => {
694                        let mut p = *params;
695                        p.outer = scale(p.outer);
696                        dev.queue
697                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
698                    }
699                    Step::FusedResidualRmsNorm { params } => {
700                        let mut p = *params;
701                        p.outer = scale(p.outer);
702                        dev.queue
703                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
704                    }
705                    Step::MatmulQkv { params, kind: _ } => {
706                        let mut p = *params;
707                        p.m = scale(p.m);
708                        dev.queue
709                            .write_buffer(&self.uniforms[gpu_ui], 0, bytemuck::bytes_of(&p));
710                    }
711                    #[cfg(feature = "splat")]
712                    Step::GaussianSplatRender { .. }
713                    | Step::GaussianSplatRenderBackward { .. }
714                    | Step::GaussianSplatPrepare { .. }
715                    | Step::GaussianSplatRasterize { .. } => {}
716                }
717                if !matches!(step, Step::FftGpu { .. }) {
718                    gpu_ui += 1;
719                }
720            }
721            self.uniforms_active_extent = Some(active);
722        }
723
724        // Encode + submit.
725        let mm_k = matmul_kernel(&dev.device);
726        let mm_w_active = matmul_wide_active_kernel(&dev.device);
727        let mm_f16w = matmul_f16w_kernel(&dev.device);
728        let mm_f16c = matmul_f16_compute_kernel(&dev.device);
729        let mm_coop = matmul_coop16_kernel(&dev.device);
730        let mm_coop_f16_vk = matmul_coop_f16_vulkan_kernel(&dev.device);
731        let mm_coop_f32 = matmul_coop_f32_active_kernel(&dev.device);
732        let mm_cast = cast_f32_to_f16_kernel(&dev.device);
733        let bk = binary_kernel(&dev.device);
734        let uk = unary_kernel(&dev.device);
735        let ck = compare_kernel(&dev.device);
736        let wk = where_kernel(&dev.device);
737        let fk = fma_kernel(&dev.device);
738        let mut step_i = 0;
739        let mut gpu_bi = 0usize;
740        let mut fft_i = 0usize;
741        while step_i < self.schedule.len() {
742            let mut enc = dev
743                .device
744                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
745                    label: Some("rlx-wgpu run"),
746                });
747            {
748                let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
749                    label: Some("rlx-wgpu compute pass"),
750                    timestamp_writes: None,
751                });
752                let mut pass_dispatched = false;
753                while step_i < self.schedule.len() {
754                    if step_is_tail_host(&self.schedule[step_i]) {
755                        step_i += 1;
756                        continue;
757                    }
758                    if step_runs_on_host(&self.schedule[step_i]) {
759                        break;
760                    }
761                    // Vulkan/DX12: end the pass after unary/cast so f32→f16
762                    // mirrors are visible to the next step. Only split once
763                    // we've dispatched in *this* pass — otherwise the step that
764                    // needs the flush would never run (infinite empty passes).
765                    if pass_dispatched
766                        && step_i > 0
767                        && step_needs_pass_flush(&self.schedule[step_i], &self.schedule[step_i - 1])
768                    {
769                        break;
770                    }
771                    let step = &self.schedule[step_i];
772                    // PLAN L3: per-step Perfetto trace span; no-op when
773                    // env var RLX_TRACE_PERFETTO unset.
774                    let _perf = rlx_ir::perfetto::TraceSpan::new(step_name(step), "wgpu");
775                    match step {
776                        Step::CastF32ToF16 { params } => {
777                            // Pre-pass for matmul_coop16: mirror f32 arena
778                            // region into f16 shadow buffer so the matmul
779                            // kernel can read A as f16. One thread per
780                            // element; 64-thread workgroups.
781                            if let Some(cast_k) = mm_cast {
782                                pass.set_pipeline(&cast_k.pipeline);
783                                pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
784                                let (gx, gy, gz) = dispatch_dims(params.len, 64);
785                                pass.dispatch_workgroups(gx, gy, gz);
786                            }
787                        }
788                        Step::Matmul {
789                            m,
790                            n,
791                            batch,
792                            b_off_f32,
793                            b_is_param,
794                            compute_precision,
795                            ..
796                        } =>
797                        // The dispatch branches below use a chain of
798                        // `is_some() && …unwrap()` to pick a pipeline
799                        // because each variant cares about a different
800                        // Option<Pipeline>. `if let Some(p) = …` chains
801                        // would require nesting per variant; the flat
802                        // form is the readable shape here.
803                        {
804                            #[allow(clippy::unnecessary_unwrap)]
805                            // Safe at any batch (see safe_for_active_extent
806                            // comment); scale m, output rows past m_s per
807                            // batch retain prior values via c_batch_stride.
808                            let m_s = scale(*m);
809                            if m_s == 0 {
810                                continue;
811                            }
812                            let coop_f16_wide = mm_coop_f16_vk.is_some()
813                                && *compute_precision == MatmulCompute::CoopF16Vk
814                                && crate::coop_f16_vk::use_wide_matmul(
815                                    *b_off_f32,
816                                    *n,
817                                    &self.coop_f16_b_param,
818                                    &self.coop_f16_vk_wide_b,
819                                );
820                            pass.set_bind_group(
821                                0,
822                                coop_f16_vk_bind_group(self, gpu_bi, coop_f16_wide),
823                                &[],
824                            );
825                            // Kernel selection priority:
826                            //   1. compute_precision == F16 + b_is_param +
827                            //      SHADER_F16 → matmul_f16_compute
828                            //      (f16 multiply, f32 acc — 2× ALU on Apple)
829                            //   2. legacy RLX_WGPU_F16_WEIGHTS opt-in →
830                            //      matmul_f16w (storage-only f16; experimental,
831                            //      currently regresses on Apple)
832                            //   3. wide-N (m≥32, n≥64)   → matmul_wide
833                            //   4. otherwise            → matmul (small/skinny)
834                            let f16w_opt_in = rlx_ir::env::flag("RLX_WGPU_F16_WEIGHTS");
835                            if let Some(coop) = mm_coop.as_ref()
836                                && *b_is_param
837                                && *compute_precision == MatmulCompute::Coop16
838                            {
839                                // Hardware GEMM via simdgroup_matrix /
840                                // KHR_cooperative_matrix. 32×32 output tile
841                                // per workgroup (16 hardware-GEMM ops with
842                                // shared A/B loads). Caller guaranteed m, n,
843                                // k are multiples of 32/32/8.
844                                pass.set_pipeline(&coop.pipeline);
845                                pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), *batch);
846                            } else if mm_coop_f16_vk.is_some()
847                                && *compute_precision == MatmulCompute::CoopF16Vk
848                            {
849                                if coop_f16_wide {
850                                    dispatch_wide_f32_matmul(
851                                        &mut pass,
852                                        mm_w_active,
853                                        mm_k,
854                                        m_s,
855                                        *n,
856                                        *batch,
857                                    );
858                                } else {
859                                    let n_eff = scale(*n);
860                                    let coop_vk =
861                                        matmul_coop_f16_vulkan_active_kernel(&dev.device, n_eff)
862                                            .expect("coop f16 vk kernel missing");
863                                    pass.set_pipeline(&coop_vk.pipeline);
864                                    pass.dispatch_workgroups(
865                                        m_s.div_ceil(16),
866                                        n.div_ceil(16),
867                                        *batch,
868                                    );
869                                }
870                            } else if let Some(coop_f32) = mm_coop_f32.as_ref()
871                                && *b_is_param
872                                && *compute_precision == MatmulCompute::CoopF32
873                            {
874                                // CoopF32: Metal uses 32×32 simdgroup tiles;
875                                // Vulkan uses 8×8 coopLoadT portable kernel.
876                                pass.set_pipeline(&coop_f32.pipeline);
877                                let backend = wgpu_device()
878                                    .map(|d| d.backend)
879                                    .unwrap_or(wgpu::Backend::Noop);
880                                let (gx, gy) = if backend == wgpu::Backend::Metal {
881                                    (n.div_ceil(32), m_s.div_ceil(32))
882                                } else {
883                                    (m_s.div_ceil(8), n.div_ceil(8))
884                                };
885                                pass.dispatch_workgroups(gx, gy, *batch);
886                            } else if let Some(f16c) = mm_f16c.as_ref()
887                                && *b_is_param
888                                && *compute_precision == MatmulCompute::F16
889                            {
890                                pass.set_pipeline(&f16c.pipeline);
891                                pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), *batch);
892                            } else if let Some(f16w) = mm_f16w.as_ref()
893                                && *b_is_param
894                                && f16w_opt_in
895                            {
896                                pass.set_pipeline(&f16w.pipeline);
897                                pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), *batch);
898                            } else if m_s >= 32 && *n >= 64 {
899                                pass.set_pipeline(&mm_w_active.pipeline);
900                                let backend = wgpu_device()
901                                    .map(|d| d.backend)
902                                    .unwrap_or(wgpu::Backend::Noop);
903                                let (gx, gy) = if matches!(
904                                    backend,
905                                    wgpu::Backend::Vulkan | wgpu::Backend::Dx12
906                                ) {
907                                    (n.div_ceil(64), m_s.div_ceil(64))
908                                } else {
909                                    (n.div_ceil(64), m_s.div_ceil(32))
910                                };
911                                pass.dispatch_workgroups(gx, gy, *batch);
912                            } else {
913                                pass.set_pipeline(&mm_k.pipeline);
914                                pass.dispatch_workgroups(n.div_ceil(32), m_s.div_ceil(32), *batch);
915                            }
916                        }
917                        Step::Binary { params } => {
918                            let n_s = scale(params.n);
919                            if n_s == 0 {
920                                continue;
921                            }
922                            pass.set_pipeline(&bk.pipeline);
923                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
924                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
925                            pass.dispatch_workgroups(gx, gy, gz);
926                        }
927                        Step::Compare { params } => {
928                            let n_s = scale(params.n);
929                            if n_s == 0 {
930                                continue;
931                            }
932                            pass.set_pipeline(&ck.pipeline);
933                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
934                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
935                            pass.dispatch_workgroups(gx, gy, gz);
936                        }
937                        Step::Unary { params, f16_mirror } => {
938                            let n_s = scale(params.n);
939                            if n_s == 0 {
940                                continue;
941                            }
942                            if *f16_mirror {
943                                if let Some(uk_f16) = unary_f16_mirror_kernel(&dev.device) {
944                                    pass.set_pipeline(&uk_f16.pipeline);
945                                } else {
946                                    pass.set_pipeline(&uk.pipeline);
947                                }
948                            } else {
949                                pass.set_pipeline(&uk.pipeline);
950                            }
951                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
952                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
953                            pass.dispatch_workgroups(gx, gy, gz);
954                        }
955                        Step::Where { params } => {
956                            let n_s = scale(params.n);
957                            if n_s == 0 {
958                                continue;
959                            }
960                            pass.set_pipeline(&wk.pipeline);
961                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
962                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
963                            pass.dispatch_workgroups(gx, gy, gz);
964                        }
965                        Step::Fma { params } => {
966                            let n_s = scale(params.n);
967                            if n_s == 0 {
968                                continue;
969                            }
970                            pass.set_pipeline(&fk.pipeline);
971                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
972                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
973                            pass.dispatch_workgroups(gx, gy, gz);
974                        }
975                        Step::Reduce { params } => {
976                            let outer_s = scale(params.outer);
977                            if outer_s == 0 {
978                                continue;
979                            }
980                            let rk = reduce_kernel(&dev.device);
981                            pass.set_pipeline(&rk.pipeline);
982                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
983                            let total_out = outer_s.saturating_mul(params.inner);
984                            if params.reduce_dim <= 64 {
985                                // Fast path: 1 thread per output cell.
986                                let (gx, gy, gz) = dispatch_dims(total_out, 64);
987                                pass.dispatch_workgroups(gx, gy, gz);
988                            } else {
989                                // Tree-reduce path: 1 workgroup (64
990                                // threads) per output cell, parallel
991                                // reduction with shared scratch.
992                                let (gx, gy, gz) = dispatch_dims(total_out, 1);
993                                pass.dispatch_workgroups(gx, gy, gz);
994                            }
995                        }
996                        Step::Softmax { params } => {
997                            let outer_s = scale(params.outer);
998                            if outer_s == 0 {
999                                continue;
1000                            }
1001                            let sk = softmax_kernel(&dev.device);
1002                            pass.set_pipeline(&sk.pipeline);
1003                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1004                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1005                            pass.dispatch_workgroups(gx, gy, gz);
1006                        }
1007                        Step::SoftmaxCrossEntropy { params } => {
1008                            let outer_s = scale(params.outer);
1009                            if outer_s == 0 {
1010                                continue;
1011                            }
1012                            let sk = softmax_cross_entropy_kernel(&dev.device);
1013                            pass.set_pipeline(&sk.pipeline);
1014                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1015                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1016                            pass.dispatch_workgroups(gx, gy, gz);
1017                        }
1018                        Step::LayerNorm { params } => {
1019                            let outer_s = scale(params.outer);
1020                            if outer_s == 0 {
1021                                continue;
1022                            }
1023                            let lk = layernorm_kernel(&dev.device);
1024                            pass.set_pipeline(&lk.pipeline);
1025                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1026                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1027                            pass.dispatch_workgroups(gx, gy, gz);
1028                        }
1029                        Step::RmsNormBackwardInput { params } => {
1030                            let outer_s = scale(params.outer);
1031                            if outer_s == 0 {
1032                                continue;
1033                            }
1034                            let rk = rms_norm_backward_kernel(&dev.device);
1035                            pass.set_pipeline(&rk.pipeline);
1036                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1037                            pass.dispatch_workgroups(outer_s, 1, 1);
1038                        }
1039                        Step::RmsNormBackwardGamma { params }
1040                        | Step::RmsNormBackwardBeta { params } => {
1041                            if params.inner == 0 {
1042                                continue;
1043                            }
1044                            let rk = rms_norm_backward_param_kernel(&dev.device);
1045                            pass.set_pipeline(&rk.pipeline);
1046                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1047                            pass.dispatch_workgroups(1, 1, 1);
1048                        }
1049                        Step::LayerNormBackwardInput { params } => {
1050                            let outer_s = scale(params.outer);
1051                            if outer_s == 0 {
1052                                continue;
1053                            }
1054                            let lk = layer_norm_backward_input_kernel(&dev.device);
1055                            pass.set_pipeline(&lk.pipeline);
1056                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1057                            pass.dispatch_workgroups(outer_s, 1, 1);
1058                        }
1059                        Step::LayerNormBackwardGammaPartial {
1060                            params,
1061                            num_workgroups,
1062                        } => {
1063                            if params.inner == 0 || *num_workgroups == 0 {
1064                                continue;
1065                            }
1066                            let lk = layer_norm_backward_gamma_partial_kernel(&dev.device);
1067                            pass.set_pipeline(&lk.pipeline);
1068                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1069                            pass.dispatch_workgroups(*num_workgroups, 1, 1);
1070                        }
1071                        Step::LayerNormBackwardGammaReduce { params } => {
1072                            if params.inner == 0 {
1073                                continue;
1074                            }
1075                            let lk = layer_norm_backward_gamma_reduce_kernel(&dev.device);
1076                            pass.set_pipeline(&lk.pipeline);
1077                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1078                            pass.dispatch_workgroups(1, 1, 1);
1079                        }
1080                        Step::CumsumBackward { params } => {
1081                            let outer_s = scale(params.outer);
1082                            if outer_s == 0 {
1083                                continue;
1084                            }
1085                            let ck = cumsum_backward_kernel(&dev.device);
1086                            pass.set_pipeline(&ck.pipeline);
1087                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1088                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1089                            pass.dispatch_workgroups(gx, gy, gz);
1090                        }
1091                        Step::RopeBackward { params } => {
1092                            let seq_s = scale(params.seq);
1093                            if seq_s == 0 {
1094                                continue;
1095                            }
1096                            let rk = rope_backward_kernel(&dev.device);
1097                            pass.set_pipeline(&rk.pipeline);
1098                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1099                            let total = params.batch * seq_s * params.hidden;
1100                            let (gx, gy, gz) = dispatch_dims(total, 64);
1101                            pass.dispatch_workgroups(gx, gy, gz);
1102                        }
1103                        Step::GatherBackward { params } => {
1104                            let outer_s = scale(params.outer);
1105                            if outer_s == 0 {
1106                                continue;
1107                            }
1108                            let total = outer_s * params.axis_dim * params.trailing;
1109                            if total > 0 {
1110                                let zk = gather_backward_zero_kernel(&dev.device);
1111                                pass.set_pipeline(&zk.pipeline);
1112                                pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1113                                let (gx, _, _) = dispatch_dims(total, 256);
1114                                pass.dispatch_workgroups(gx, 1, 1);
1115                            }
1116                            let ak = gather_backward_acc_kernel(&dev.device);
1117                            pass.set_pipeline(&ak.pipeline);
1118                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1119                            pass.dispatch_workgroups(outer_s, 1, 1);
1120                        }
1121                        Step::Cumsum { params } => {
1122                            let outer_s = scale(params.outer);
1123                            if outer_s == 0 {
1124                                continue;
1125                            }
1126                            let ck2 = cumsum_kernel(&dev.device);
1127                            pass.set_pipeline(&ck2.pipeline);
1128                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1129                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1130                            pass.dispatch_workgroups(gx, gy, gz);
1131                        }
1132                        Step::FftGpu {
1133                            src_off,
1134                            dst_off,
1135                            outer,
1136                            n,
1137                            inverse,
1138                            norm_scale,
1139                        } => {
1140                            let res = &self.fft_gpu_steps[fft_i];
1141                            fft_i += 1;
1142                            crate::fft_dispatch::dispatch_fft_gpu_in_pass(
1143                                &dev.device,
1144                                &dev.queue,
1145                                &mut pass,
1146                                res,
1147                                *src_off,
1148                                *dst_off,
1149                                *outer,
1150                                *n,
1151                                *inverse != 0,
1152                                *norm_scale,
1153                            );
1154                        }
1155                        Step::Copy { params } => {
1156                            let n_s = scale(params.n);
1157                            if n_s == 0 {
1158                                continue;
1159                            }
1160                            let ck2 = copy_kernel(&dev.device);
1161                            pass.set_pipeline(&ck2.pipeline);
1162                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1163                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
1164                            pass.dispatch_workgroups(gx, gy, gz);
1165                        }
1166                        Step::BufferCopy { .. } => {
1167                            // Host step: `copy_buffer_to_buffer` runs outside compute passes.
1168                        }
1169                        Step::ElementwiseRegion { params } => {
1170                            let len_s = scale(params.len);
1171                            if len_s == 0 {
1172                                continue;
1173                            }
1174                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1175                            if params.prologue == rlx_ir::REGION_PROLOGUE_RESIZE_NEAREST_2X_NCHW {
1176                                let ek = elementwise_region_spatial_kernel(&dev.device);
1177                                pass.set_pipeline(&ek.pipeline);
1178                                let (gx, gy, gz) = dispatch_prologue_nchw(
1179                                    params.out_w,
1180                                    params.out_h,
1181                                    params.out_n * params.out_c,
1182                                );
1183                                pass.dispatch_workgroups(gx, gy, gz);
1184                            } else {
1185                                let ek = elementwise_region_kernel(&dev.device);
1186                                pass.set_pipeline(&ek.pipeline);
1187                                let (gx, gy, gz) = dispatch_dims(len_s, 64);
1188                                pass.dispatch_workgroups(gx, gy, gz);
1189                            }
1190                        }
1191                        Step::BatchElementwiseRegion { params } => {
1192                            let slice_len_s = scale(params.slice_len);
1193                            let num_batch_s = scale(params.num_batch);
1194                            if slice_len_s == 0 || num_batch_s == 0 {
1195                                continue;
1196                            }
1197                            let ek = batch_elementwise_region_kernel(&dev.device);
1198                            pass.set_pipeline(&ek.pipeline);
1199                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1200                            let (gx, gy, _) = dispatch_dims(slice_len_s, 64);
1201                            pass.dispatch_workgroups(gx, gy, num_batch_s);
1202                        }
1203                        Step::Transpose { params, .. } => {
1204                            // Compute scaled grid count to match the
1205                            // uniform's scaled out_total when bucket axis
1206                            // is outermost.
1207                            let total_s = if params.bucket_outermost == 1 && params.out_dim_0 > 0 {
1208                                let scaled_d0 = scale(params.out_dim_0);
1209                                let inner = params.out_total / params.out_dim_0;
1210                                scaled_d0 * inner
1211                            } else {
1212                                params.out_total
1213                            };
1214                            if total_s == 0 {
1215                                continue;
1216                            }
1217                            let tk = transpose_kernel(&dev.device);
1218                            pass.set_pipeline(&tk.pipeline);
1219                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1220                            let (gx, gy, gz) = dispatch_dims(total_s, 64);
1221                            pass.dispatch_workgroups(gx, gy, gz);
1222                        }
1223                        Step::Narrow { params } => {
1224                            let total_s = scale(params.total);
1225                            if total_s == 0 {
1226                                continue;
1227                            }
1228                            let nk = narrow_kernel(&dev.device);
1229                            pass.set_pipeline(&nk.pipeline);
1230                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1231                            let (gx, gy, gz) = dispatch_dims(total_s, 64);
1232                            pass.dispatch_workgroups(gx, gy, gz);
1233                        }
1234                        Step::Concat { params } => {
1235                            let total_s = scale(params.total);
1236                            if total_s == 0 {
1237                                continue;
1238                            }
1239                            let cck = concat_kernel(&dev.device);
1240                            pass.set_pipeline(&cck.pipeline);
1241                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1242                            let (gx, gy, gz) = dispatch_dims(total_s, 64);
1243                            pass.dispatch_workgroups(gx, gy, gz);
1244                        }
1245                        Step::Gather { params } => {
1246                            let n_out_s = scale(params.n_out);
1247                            if n_out_s == 0 {
1248                                continue;
1249                            }
1250                            let gk = gather_kernel(&dev.device);
1251                            pass.set_pipeline(&gk.pipeline);
1252                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1253                            let (gx, gy, gz) = dispatch_dims(n_out_s, 64);
1254                            pass.dispatch_workgroups(gx, gy, gz);
1255                        }
1256                        Step::GatherAxis { params } => {
1257                            let total_s = scale(params.total);
1258                            if total_s == 0 {
1259                                continue;
1260                            }
1261                            let gk = gather_axis_kernel(&dev.device);
1262                            pass.set_pipeline(&gk.pipeline);
1263                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1264                            let (gx, gy, gz) = dispatch_dims(total_s, 64);
1265                            pass.dispatch_workgroups(gx, gy, gz);
1266                        }
1267                        Step::Attention { params, .. } => {
1268                            // Scale seq_q for grid dim; per-head strides
1269                            // come from seq_q_stride / seq_k_stride (full
1270                            // extent) inside the WGSL.
1271                            let seq_q_s = scale(params.seq_q);
1272                            if seq_q_s == 0 {
1273                                continue;
1274                            }
1275                            let ak = attention_kernel(&dev.device);
1276                            pass.set_pipeline(&ak.pipeline);
1277                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1278                            let total = params.batch * params.heads * seq_q_s;
1279                            let (gx, gy, gz) = dispatch_dims(total, 64);
1280                            pass.dispatch_workgroups(gx, gy, gz);
1281                        }
1282                        Step::AttentionBackward { params, .. } => {
1283                            let axis = if params.wrt == 0 {
1284                                params.seq_q
1285                            } else {
1286                                params.seq_k
1287                            };
1288                            let axis_s = scale(axis);
1289                            if axis_s == 0 {
1290                                continue;
1291                            }
1292                            let ak = attention_bwd_kernel(&dev.device);
1293                            pass.set_pipeline(&ak.pipeline);
1294                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1295                            let total = params.batch * params.heads * axis_s;
1296                            let (gx, gy, gz) = dispatch_dims(total, 64);
1297                            pass.dispatch_workgroups(gx, gy, gz);
1298                        }
1299                        Step::Rope { params } => {
1300                            // Multi-batch via stride-field WGSL fix:
1301                            // iterate `batch * scaled_seq * last_dim` items.
1302                            let s_active = scale(params.seq);
1303                            let total_s = params.batch * s_active * params.last_dim;
1304                            if total_s == 0 {
1305                                continue;
1306                            }
1307                            let rk = rope_kernel(&dev.device);
1308                            pass.set_pipeline(&rk.pipeline);
1309                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1310                            let (gx, gy, gz) = dispatch_dims(total_s, 64);
1311                            pass.dispatch_workgroups(gx, gy, gz);
1312                        }
1313                        Step::Expand { params, .. } => {
1314                            let total_s = if params.bucket_outermost == 1 && params.out_dim_0 > 0 {
1315                                let scaled_d0 = scale(params.out_dim_0);
1316                                let inner = params.out_total / params.out_dim_0;
1317                                scaled_d0 * inner
1318                            } else {
1319                                params.out_total
1320                            };
1321                            if total_s == 0 {
1322                                continue;
1323                            }
1324                            let ek = expand_kernel(&dev.device);
1325                            pass.set_pipeline(&ek.pipeline);
1326                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1327                            let (gx, gy, gz) = dispatch_dims(total_s, 64);
1328                            pass.dispatch_workgroups(gx, gy, gz);
1329                        }
1330                        Step::Argmax { params } => {
1331                            let outer_s = scale(params.outer);
1332                            if outer_s == 0 {
1333                                continue;
1334                            }
1335                            let amk = argmax_kernel(&dev.device);
1336                            pass.set_pipeline(&amk.pipeline);
1337                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1338                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1339                            pass.dispatch_workgroups(gx, gy, gz);
1340                        }
1341                        Step::Pool2d { params } => {
1342                            let n_s = scale(params.n);
1343                            if n_s == 0 {
1344                                continue;
1345                            }
1346                            let pk = pool2d_kernel(&dev.device);
1347                            pass.set_pipeline(&pk.pipeline);
1348                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1349                            let total = n_s * params.c * params.h_out * params.w_out;
1350                            let (gx, gy, gz) = dispatch_dims(total, 64);
1351                            pass.dispatch_workgroups(gx, gy, gz);
1352                        }
1353                        Step::Conv2d { params } => {
1354                            let n_s = scale(params.n);
1355                            if n_s == 0 {
1356                                continue;
1357                            }
1358                            let ck2 = conv2d_kernel(&dev.device);
1359                            pass.set_pipeline(&ck2.pipeline);
1360                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1361                            // conv2d.wgsl tiles `CONV2D_TILE` output spatial
1362                            // positions per thread (const must match the kernel).
1363                            let spatial = params.h_out * params.w_out;
1364                            let sp_tiles = spatial.div_ceil(CONV2D_TILE);
1365                            let total = n_s * params.c_out * sp_tiles;
1366                            let (gx, gy, gz) = dispatch_dims(total, 64);
1367                            pass.dispatch_workgroups(gx, gy, gz);
1368                        }
1369                        Step::Pool1d { params } => {
1370                            let n_s = scale(params.n);
1371                            if n_s == 0 {
1372                                continue;
1373                            }
1374                            let pk = pool1d_kernel(&dev.device);
1375                            pass.set_pipeline(&pk.pipeline);
1376                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1377                            let total = n_s * params.c * params.l_out;
1378                            let (gx, gy, gz) = dispatch_dims(total, 64);
1379                            pass.dispatch_workgroups(gx, gy, gz);
1380                        }
1381                        Step::Pool3d { params } => {
1382                            let n_s = scale(params.n);
1383                            if n_s == 0 {
1384                                continue;
1385                            }
1386                            let pk = pool3d_kernel(&dev.device);
1387                            pass.set_pipeline(&pk.pipeline);
1388                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1389                            let total = n_s * params.c * params.d_out * params.h_out * params.w_out;
1390                            let (gx, gy, gz) = dispatch_dims(total, 64);
1391                            pass.dispatch_workgroups(gx, gy, gz);
1392                        }
1393                        Step::Conv1d { params } => {
1394                            let n_s = scale(params.n);
1395                            if n_s == 0 {
1396                                continue;
1397                            }
1398                            let ck = conv1d_kernel(&dev.device);
1399                            pass.set_pipeline(&ck.pipeline);
1400                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1401                            let total = n_s * params.c_out * params.l_out;
1402                            let (gx, gy, gz) = dispatch_dims(total, 64);
1403                            pass.dispatch_workgroups(gx, gy, gz);
1404                        }
1405                        Step::Conv3d { params } => {
1406                            let n_s = scale(params.n);
1407                            if n_s == 0 {
1408                                continue;
1409                            }
1410                            let ck = conv3d_kernel(&dev.device);
1411                            pass.set_pipeline(&ck.pipeline);
1412                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1413                            let total =
1414                                n_s * params.c_out * params.d_out * params.h_out * params.w_out;
1415                            let (gx, gy, gz) = dispatch_dims(total, 64);
1416                            pass.dispatch_workgroups(gx, gy, gz);
1417                        }
1418                        Step::ScatterAdd { params } => {
1419                            let sk = scatter_add_kernel(&dev.device);
1420                            pass.set_pipeline(&sk.pipeline);
1421                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1422                            // Phase 0 zeros the FULL output (preserves
1423                            // accumulator semantics). Phase 1 scatters first
1424                            // num_updates_active updates only; serial single
1425                            // workgroup either way (atomic CAS unsupported in
1426                            // naga's MSL emitter — see scatter_add.wgsl).
1427                            if params.op == 0 {
1428                                let (gx, gy, gz) = dispatch_dims(params.out_total, 64);
1429                                pass.dispatch_workgroups(gx, gy, gz);
1430                            } else {
1431                                pass.dispatch_workgroups(1, 1, 1);
1432                            }
1433                        }
1434                        Step::TopK { params } => {
1435                            let outer_s = scale(params.outer);
1436                            if outer_s == 0 {
1437                                continue;
1438                            }
1439                            let tk = topk_kernel(&dev.device);
1440                            pass.set_pipeline(&tk.pipeline);
1441                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1442                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1443                            pass.dispatch_workgroups(gx, gy, gz);
1444                        }
1445                        Step::WelchPeaksGpu { params } => {
1446                            let batch_s = scale(params.welch_batch);
1447                            if batch_s == 0 {
1448                                continue;
1449                            }
1450                            let wk = welch_peaks_gpu_kernel(&dev.device);
1451                            pass.set_pipeline(&wk.pipeline);
1452                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1453                            let (gx, gy, gz) = dispatch_dims(batch_s, 64);
1454                            pass.dispatch_workgroups(gx, gy, gz);
1455                        }
1456                        Step::UmapKnn { params } => {
1457                            let n_s = scale(params.n);
1458                            if n_s == 0 {
1459                                continue;
1460                            }
1461                            let uk = umap_knn_kernel(&dev.device);
1462                            pass.set_pipeline(&uk.pipeline);
1463                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1464                            let (gx, gy, gz) = dispatch_dims(n_s, 64);
1465                            pass.dispatch_workgroups(gx, gy, gz);
1466                        }
1467                        Step::GroupedMatmul { params } => {
1468                            let m_s = scale(params.m);
1469                            if m_s == 0 {
1470                                continue;
1471                            }
1472                            let gk = grouped_matmul_kernel(&dev.device);
1473                            pass.set_pipeline(&gk.pipeline);
1474                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1475                            pass.dispatch_workgroups(params.n.div_ceil(8), m_s.div_ceil(8), 1);
1476                        }
1477                        Step::Sample { params } => {
1478                            let outer_s = scale(params.outer);
1479                            if outer_s == 0 {
1480                                continue;
1481                            }
1482                            let sk = sample_kernel(&dev.device);
1483                            pass.set_pipeline(&sk.pipeline);
1484                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1485                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1486                            pass.dispatch_workgroups(gx, gy, gz);
1487                        }
1488                        Step::SelectiveScan { params } => {
1489                            // Predicate-gated to batch=1; the seq scaling
1490                            // happens inside the kernel (uniform sees scaled
1491                            // seq). Dispatch grid here is per-(batch, hidden);
1492                            // unaffected by seq scaling.
1493                            let ssk = selective_scan_kernel(&dev.device);
1494                            pass.set_pipeline(&ssk.pipeline);
1495                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1496                            let total = params.batch * params.hidden;
1497                            let (gx, gy, gz) = dispatch_dims(total, 64);
1498                            pass.dispatch_workgroups(gx, gy, gz);
1499                        }
1500                        Step::Mamba2 { params } => {
1501                            let mk = mamba2_kernel(&dev.device);
1502                            pass.set_pipeline(&mk.pipeline);
1503                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1504                            let total = params.batch * params.heads * params.head_dim;
1505                            let (gx, gy, gz) = dispatch_dims(total, 64);
1506                            pass.dispatch_workgroups(gx, gy, gz);
1507                        }
1508                        Step::Gru { params } => {
1509                            // One workgroup per batch item (workgroup_size=256).
1510                            let gk = gru_kernel(&dev.device);
1511                            pass.set_pipeline(&gk.pipeline);
1512                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1513                            pass.dispatch_workgroups(params.batch, 1, 1);
1514                        }
1515                        Step::Rnn { params } => {
1516                            let rk = rnn_kernel(&dev.device);
1517                            pass.set_pipeline(&rk.pipeline);
1518                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1519                            pass.dispatch_workgroups(params.batch, 1, 1);
1520                        }
1521                        Step::DequantMatmul { params } => {
1522                            let m_s = scale(params.m);
1523                            if m_s == 0 {
1524                                continue;
1525                            }
1526                            let dk = dequant_matmul_kernel(&dev.device);
1527                            pass.set_pipeline(&dk.pipeline);
1528                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1529                            pass.dispatch_workgroups(params.n.div_ceil(8), m_s.div_ceil(8), 1);
1530                        }
1531                        Step::FusedResidualLn { params } => {
1532                            let outer_s = scale(params.outer);
1533                            if outer_s == 0 {
1534                                continue;
1535                            }
1536                            let frk = fused_residual_ln_kernel(&dev.device);
1537                            pass.set_pipeline(&frk.pipeline);
1538                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1539                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1540                            pass.dispatch_workgroups(gx, gy, gz);
1541                        }
1542                        Step::FusedResidualLnTee { params } => {
1543                            let outer_s = scale(params.outer);
1544                            if outer_s == 0 {
1545                                continue;
1546                            }
1547                            let frtk = fused_residual_ln_tee_kernel(&dev.device);
1548                            pass.set_pipeline(&frtk.pipeline);
1549                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1550                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1551                            pass.dispatch_workgroups(gx, gy, gz);
1552                        }
1553                        Step::FusedResidualRmsNorm { params } => {
1554                            let outer_s = scale(params.outer);
1555                            if outer_s == 0 {
1556                                continue;
1557                            }
1558                            let frk = fused_residual_rms_norm_kernel(&dev.device);
1559                            pass.set_pipeline(&frk.pipeline);
1560                            pass.set_bind_group(0, &self.bind_groups[gpu_bi], &[]);
1561                            let (gx, gy, gz) = dispatch_dims(outer_s, 64);
1562                            pass.dispatch_workgroups(gx, gy, gz);
1563                        }
1564                        Step::MatmulQkv { params, kind } => {
1565                            let m_s = scale(params.m);
1566                            if m_s == 0 {
1567                                continue;
1568                            }
1569                            let qkv_coop_wide = matches!(kind, MatmulQkvKind::CoopF16Vk)
1570                                && crate::coop_f16_vk::use_wide_matmul(
1571                                    params.b_off,
1572                                    params.n,
1573                                    &self.coop_f16_b_param,
1574                                    &self.coop_f16_vk_wide_b,
1575                                );
1576                            pass.set_bind_group(
1577                                0,
1578                                coop_f16_vk_bind_group(self, gpu_bi, qkv_coop_wide),
1579                                &[],
1580                            );
1581                            match kind {
1582                                MatmulQkvKind::CoopF16Vk => {
1583                                    if qkv_coop_wide {
1584                                        pass.set_pipeline(&matmul_qkv_kernel(&dev.device).pipeline);
1585                                        pass.dispatch_workgroups(
1586                                            params.n.div_ceil(32),
1587                                            m_s.div_ceil(32),
1588                                            1,
1589                                        );
1590                                    } else {
1591                                        let n_eff = scale(params.n);
1592                                        let mqk = matmul_qkv_coop_f16_vk_active_kernel(
1593                                            &dev.device,
1594                                            n_eff,
1595                                        )
1596                                        .expect("coop f16 matmul_qkv kernel missing");
1597                                        pass.set_pipeline(&mqk.pipeline);
1598                                        pass.dispatch_workgroups(
1599                                            m_s.div_ceil(16),
1600                                            params.n.div_ceil(16),
1601                                            1,
1602                                        );
1603                                    }
1604                                }
1605                                MatmulQkvKind::CoopF32 => {
1606                                    pass.set_pipeline(
1607                                        &matmul_qkv_coop_f32_kernel(&dev.device)
1608                                            .expect("coop matmul_qkv kernel missing")
1609                                            .pipeline,
1610                                    );
1611                                    pass.dispatch_workgroups(
1612                                        params.n.div_ceil(32),
1613                                        m_s.div_ceil(32),
1614                                        1,
1615                                    );
1616                                }
1617                                MatmulQkvKind::F32 => {
1618                                    pass.set_pipeline(&matmul_qkv_kernel(&dev.device).pipeline);
1619                                    pass.dispatch_workgroups(
1620                                        params.n.div_ceil(32),
1621                                        m_s.div_ceil(32),
1622                                        1,
1623                                    );
1624                                }
1625                            }
1626                        }
1627                        Step::GatherSplit { .. }
1628                        | Step::DequantMatmulGguf { .. }
1629                        | Step::DequantGroupedMatmulGguf { .. }
1630                        | Step::GatedDeltaNet { .. }
1631                        | Step::Lstm { .. }
1632                        | Step::ConvTranspose2d { .. }
1633                        | Step::GroupNormHost { .. }
1634                        | Step::LayerNorm2dHost { .. }
1635                        | Step::ResizeNearest2xHost { .. }
1636                        | Step::ReverseHost { .. }
1637                        | Step::ArgReduceHost { .. }
1638                        | Step::GruHost { .. }
1639                        | Step::RnnHost { .. }
1640                        | Step::Llada2GroupLimitedGate { .. }
1641                        | Step::UmapKnnHost { .. }
1642                        | Step::MsDeformAttnHost { .. }
1643                        | Step::FftHost { .. }
1644                        | Step::ScanHost { .. }
1645                        | Step::Im2ColHost { .. }
1646                        | Step::RngNormalHost { .. }
1647                        | Step::RngUniformHost { .. }
1648                        | Step::WelchPeaksHost { .. }
1649                        | Step::LogMelHost { .. }
1650                        | Step::LogMelBackwardHost { .. } => {}
1651                        #[cfg(feature = "splat")]
1652                        Step::GaussianSplatRender { .. }
1653                        | Step::GaussianSplatRenderBackward { .. }
1654                        | Step::GaussianSplatPrepare { .. }
1655                        | Step::GaussianSplatRasterize { .. } => {}
1656                    }
1657                    if !matches!(step, Step::FftGpu { .. }) {
1658                        gpu_bi += 1;
1659                    }
1660                    step_i += 1;
1661                    pass_dispatched = true;
1662                }
1663            }
1664            let needs_f16_drain = step_i < self.schedule.len()
1665                && !step_runs_on_host(&self.schedule[step_i])
1666                && step_i > 0
1667                && step_needs_pass_flush(&self.schedule[step_i], &self.schedule[step_i - 1]);
1668            let gpu_schedule_done = step_i >= self.schedule.len();
1669            let skip_readback = rlx_ir::env::flag("RLX_BENCH_DISPATCH_ONLY") || self.dispatch_only;
1670            let defer_tail = gpu_schedule_done && self.schedule.iter().any(step_is_tail_host);
1671            let mut fused_readback: Option<(
1672                ReadbackLayout,
1673                std::sync::mpsc::Receiver<Result<(), wgpu::BufferAsyncError>>,
1674                Vec<usize>,
1675            )> = None;
1676            if gpu_schedule_done && !skip_readback && !defer_tail {
1677                if !self.gpu_handle_feeds.is_empty() {
1678                    self.propagate_gpu_handle_feeds_on_gpu(dev, &mut enc);
1679                }
1680                let plan = self.readback_plan();
1681                let out_ids_all: Vec<_> = self.graph.outputs.clone();
1682                let out_ids: Vec<_> = plan.iter().map(|&i| out_ids_all[i]).collect();
1683                let layout = ReadbackLayout::for_nodes(&self.arena, &out_ids);
1684                if use_tiny_readback(&layout, out_ids.len()) && plan == vec![0] {
1685                    if self.tiny_readback.is_none() {
1686                        self.tiny_readback = Some(TinyReadbackStaging::new(&dev.device));
1687                    }
1688                    let tiny = self.tiny_readback.as_ref().expect("tiny readback");
1689                    encode_readback_copies(&mut enc, &self.arena, tiny.buffer(), &out_ids, &layout);
1690                    let map_rx = schedule_readback_map(&mut enc, tiny.buffer(), &layout);
1691                    let sub = dev.queue.submit(std::iter::once(enc.finish()));
1692                    wait_readback_map(&dev.device, sub, &map_rx, layout.total_bytes);
1693                    map_rx.recv().unwrap().unwrap();
1694                    return self.pack_readback_outputs(
1695                        &plan,
1696                        vec![decode_tiny_mapped_f32(tiny.buffer(), layout.total_bytes)],
1697                    );
1698                }
1699                ReadbackStaging::prepare(
1700                    &dev.device,
1701                    &mut self.readback_staging,
1702                    layout.total_bytes,
1703                );
1704                if let Some(staging) = self.readback_staging.as_ref() {
1705                    encode_readback_copies(
1706                        &mut enc,
1707                        &self.arena,
1708                        staging.buffer(),
1709                        &out_ids,
1710                        &layout,
1711                    );
1712                    let map_rx = schedule_readback_map(&mut enc, staging.buffer(), &layout);
1713                    fused_readback = Some((layout, map_rx, plan));
1714                }
1715            }
1716            let main_submission = dev.queue.submit(std::iter::once(enc.finish()));
1717            if defer_tail {
1718                let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1719                self.run_tail_host_audio_ops(dev);
1720                if !skip_readback {
1721                    let mut rb_enc =
1722                        dev.device
1723                            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1724                                label: Some("rlx-wgpu readback after tail-host"),
1725                            });
1726                    if !self.gpu_handle_feeds.is_empty() {
1727                        self.propagate_gpu_handle_feeds_on_gpu(dev, &mut rb_enc);
1728                    }
1729                    let plan = self.readback_plan();
1730                    let out_ids_all: Vec<_> = self.graph.outputs.clone();
1731                    let out_ids: Vec<_> = plan.iter().map(|&i| out_ids_all[i]).collect();
1732                    let layout = ReadbackLayout::for_nodes(&self.arena, &out_ids);
1733                    if use_tiny_readback(&layout, out_ids.len()) && plan == vec![0] {
1734                        if self.tiny_readback.is_none() {
1735                            self.tiny_readback = Some(TinyReadbackStaging::new(&dev.device));
1736                        }
1737                        let tiny = self.tiny_readback.as_ref().expect("tiny readback");
1738                        encode_readback_copies(
1739                            &mut rb_enc,
1740                            &self.arena,
1741                            tiny.buffer(),
1742                            &out_ids,
1743                            &layout,
1744                        );
1745                        let map_rx = schedule_readback_map(&mut rb_enc, tiny.buffer(), &layout);
1746                        let sub = dev.queue.submit(std::iter::once(rb_enc.finish()));
1747                        wait_readback_map(&dev.device, sub, &map_rx, layout.total_bytes);
1748                        map_rx.recv().unwrap().unwrap();
1749                        return self.pack_readback_outputs(
1750                            &plan,
1751                            vec![decode_tiny_mapped_f32(tiny.buffer(), layout.total_bytes)],
1752                        );
1753                    }
1754                    ReadbackStaging::prepare(
1755                        &dev.device,
1756                        &mut self.readback_staging,
1757                        layout.total_bytes,
1758                    );
1759                    if let Some(staging) = self.readback_staging.as_ref() {
1760                        encode_readback_copies(
1761                            &mut rb_enc,
1762                            &self.arena,
1763                            staging.buffer(),
1764                            &out_ids,
1765                            &layout,
1766                        );
1767                        let map_rx = schedule_readback_map(&mut rb_enc, staging.buffer(), &layout);
1768                        let sub = dev.queue.submit(std::iter::once(rb_enc.finish()));
1769                        wait_readback_map(&dev.device, sub, &map_rx, layout.total_bytes);
1770                        map_rx.recv().unwrap().unwrap();
1771                        self.dump_node_stats_if_requested(dev);
1772                        let partial = decode_mapped_readback_f32(staging.buffer(), &layout);
1773                        return self.pack_readback_outputs(&plan, partial);
1774                    }
1775                }
1776            }
1777            if needs_f16_drain {
1778                let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1779            }
1780            let need_host_sync =
1781                step_i < self.schedule.len() && step_runs_on_host(&self.schedule[step_i]);
1782            if need_host_sync {
1783                let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1784            }
1785            if gpu_schedule_done {
1786                if skip_readback || defer_tail {
1787                    return self
1788                        .graph
1789                        .outputs
1790                        .iter()
1791                        .map(|&id| {
1792                            let n = self.graph.node(id).shape.num_elements().unwrap_or(0);
1793                            vec![0.0; n]
1794                        })
1795                        .collect();
1796                }
1797                if let (Some((layout, map_rx, plan)), Some(staging)) =
1798                    (fused_readback, self.readback_staging.as_ref())
1799                {
1800                    wait_readback_map(&dev.device, main_submission, &map_rx, layout.total_bytes);
1801                    map_rx.recv().unwrap().unwrap();
1802                    self.dump_node_stats_if_requested(dev);
1803                    let partial = decode_mapped_readback_f32(staging.buffer(), &layout);
1804                    return self.pack_readback_outputs(&plan, partial);
1805                }
1806                break;
1807            }
1808            match &self.schedule[step_i] {
1809                Step::BufferCopy {
1810                    src_byte_off,
1811                    dst_byte_off,
1812                    bytes,
1813                } => {
1814                    // wgpu forbids `copy_buffer_to_buffer` on the same buffer;
1815                    // use the generic copy compute kernel instead.
1816                    let src = *src_byte_off;
1817                    let dst = *dst_byte_off;
1818                    let nbytes = *bytes as u64;
1819                    let elems = (nbytes / 4).max(1) as u32;
1820                    let lo = src.min(dst);
1821                    let hi = src.saturating_add(nbytes).max(dst.saturating_add(nbytes));
1822                    let max_binding = dev.device.limits().max_storage_buffer_binding_size;
1823                    // On >4 GiB arenas (GGUF decode) src and dst can be more than
1824                    // `max_binding` apart, so no single ≤4 GiB compute bind window
1825                    // covers both — the kernel copy would silently read/write out
1826                    // of its window. Fall back to a host round-trip (small staging
1827                    // copies only; capped by ARENA_STAGE_CAP).
1828                    if hi.saturating_sub(lo) > max_binding {
1829                        let bytes_host = self.arena.read_bytes_range(
1830                            &dev.device,
1831                            &dev.queue,
1832                            src as usize,
1833                            nbytes as usize,
1834                        );
1835                        self.arena
1836                            .write_bytes_range(&dev.queue, dst as usize, &bytes_host);
1837                        let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1838                        step_i += 1;
1839                        continue;
1840                    }
1841                    let mut base = (lo / 256) * 256;
1842                    // The bind window must cover [base, hi]. `base` is floored to 256B
1843                    // alignment (so base ≤ lo); measuring the size from `lo` instead of
1844                    // `base` clips the window when the copy straddles a 256B boundary,
1845                    // silently dropping the tail (e.g. a 92B Bool→F32 mask copy whose
1846                    // dst sat just past the window → x_mask stayed 0).
1847                    let span = hi.saturating_sub(base).max(1);
1848                    let mut size = span.div_ceil(256) * 256;
1849                    size = size.max(256).min(max_binding);
1850                    if base.saturating_add(size) > self.arena.size as u64 {
1851                        base = (self.arena.size as u64).saturating_sub(size);
1852                        base = (base / 256) * 256;
1853                    }
1854                    let p = CopyParams {
1855                        n: elems,
1856                        in_off: (src.saturating_sub(base) / 4) as u32,
1857                        out_off: (dst.saturating_sub(base) / 4) as u32,
1858                        _p0: 0,
1859                        _p1: 0,
1860                        _p2: 0,
1861                        _p3: 0,
1862                        _p4: 0,
1863                    };
1864                    let ck = copy_kernel(&dev.device);
1865                    let u = dev.device.create_buffer(&wgpu::BufferDescriptor {
1866                        label: Some("rlx-wgpu arena_copy uniform"),
1867                        size: std::mem::size_of::<CopyParams>() as u64,
1868                        usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1869                        mapped_at_creation: false,
1870                    });
1871                    dev.queue.write_buffer(&u, 0, bytemuck::bytes_of(&p));
1872                    let bg =
1873                        bind_two_buf0_window(&dev.device, ck, &self.arena.buffer, base, size, &u);
1874                    let mut enc =
1875                        dev.device
1876                            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1877                                label: Some("rlx-wgpu arena_copy"),
1878                            });
1879                    {
1880                        let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
1881                            label: Some("rlx-wgpu arena_copy pass"),
1882                            ..Default::default()
1883                        });
1884                        pass.set_pipeline(&ck.pipeline);
1885                        pass.set_bind_group(0, &bg, &[]);
1886                        let (gx, gy, gz) = dispatch_dims(elems, 64);
1887                        pass.dispatch_workgroups(gx, gy, gz);
1888                    }
1889                    dev.queue.submit(std::iter::once(enc.finish()));
1890                    let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1891                }
1892                Step::GatherSplit {
1893                    n_out,
1894                    n_idx,
1895                    dim,
1896                    vocab,
1897                    table_byte_off,
1898                    idx_byte_off,
1899                    out_byte_off,
1900                } => {
1901                    run_gather_split(
1902                        &self.arena,
1903                        &dev.device,
1904                        &dev.queue,
1905                        *n_out,
1906                        *n_idx,
1907                        *dim,
1908                        *vocab,
1909                        *table_byte_off as usize,
1910                        *idx_byte_off as usize,
1911                        *out_byte_off as usize,
1912                    );
1913                }
1914                Step::DequantMatmulGguf {
1915                    m,
1916                    k,
1917                    n,
1918                    scheme_id,
1919                    x_byte_off,
1920                    w_byte_off,
1921                    out_byte_off,
1922                } => {
1923                    if *m == 1 && crate::gguf_gpu::gemv_supports_scheme(*scheme_id) {
1924                        // Decode GEMV: fused dequant+matmul, windowed bindings,
1925                        // no f32 scratch (handles arenas larger than the 4 GiB
1926                        // single-binding limit).
1927                        crate::gguf_gpu::run_dequant_matmul_gguf_gemv(
1928                            &self.arena,
1929                            &dev.device,
1930                            &dev.queue,
1931                            *k as usize,
1932                            *n as usize,
1933                            *scheme_id,
1934                            *x_byte_off as usize,
1935                            *w_byte_off as usize,
1936                            *out_byte_off as usize,
1937                        );
1938                    } else if self.dequant_scratch_off > 0 {
1939                        crate::gguf_gpu::run_dequant_matmul_gguf_gpu(
1940                            &self.arena,
1941                            &dev.device,
1942                            &dev.queue,
1943                            *m as usize,
1944                            *k as usize,
1945                            *n as usize,
1946                            *scheme_id,
1947                            *x_byte_off as usize,
1948                            *w_byte_off as usize,
1949                            self.dequant_scratch_off,
1950                            *out_byte_off as usize,
1951                        );
1952                    } else {
1953                        crate::gguf_host::run_dequant_matmul_gguf(
1954                            &self.arena,
1955                            &dev.device,
1956                            &dev.queue,
1957                            *m as usize,
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                    }
1966                }
1967                Step::DequantGroupedMatmulGguf {
1968                    m,
1969                    k,
1970                    n,
1971                    num_experts,
1972                    scheme_id,
1973                    x_byte_off,
1974                    w_byte_off,
1975                    idx_byte_off,
1976                    out_byte_off,
1977                } => {
1978                    if self.dequant_scratch_off > 0 {
1979                        crate::gguf_gpu::run_dequant_grouped_matmul_gguf_gpu(
1980                            &self.arena,
1981                            &dev.device,
1982                            &dev.queue,
1983                            *m as usize,
1984                            *k as usize,
1985                            *n as usize,
1986                            *num_experts as usize,
1987                            *scheme_id,
1988                            *x_byte_off as usize,
1989                            *w_byte_off as usize,
1990                            *idx_byte_off as usize,
1991                            self.dequant_scratch_off,
1992                            *out_byte_off as usize,
1993                        );
1994                    } else {
1995                        crate::gguf_host::run_dequant_grouped_matmul_gguf(
1996                            &self.arena,
1997                            &dev.device,
1998                            &dev.queue,
1999                            *m as usize,
2000                            *k as usize,
2001                            *n as usize,
2002                            *num_experts as usize,
2003                            *scheme_id,
2004                            *x_byte_off as usize,
2005                            *w_byte_off as usize,
2006                            *idx_byte_off as usize,
2007                            *out_byte_off as usize,
2008                        );
2009                    }
2010                }
2011                Step::GatedDeltaNet {
2012                    q_byte_off,
2013                    k_byte_off,
2014                    v_byte_off,
2015                    g_byte_off,
2016                    beta_byte_off,
2017                    state_byte_off,
2018                    dst_byte_off,
2019                    batch,
2020                    seq,
2021                    heads,
2022                    state_size,
2023                    use_carry,
2024                } => {
2025                    crate::gdn_host::run_gated_delta_net(
2026                        &self.arena,
2027                        &dev.device,
2028                        &dev.queue,
2029                        *q_byte_off as usize,
2030                        *k_byte_off as usize,
2031                        *v_byte_off as usize,
2032                        *g_byte_off as usize,
2033                        *beta_byte_off as usize,
2034                        *state_byte_off as usize,
2035                        *dst_byte_off as usize,
2036                        *batch as usize,
2037                        *seq as usize,
2038                        *heads as usize,
2039                        *state_size as usize,
2040                        *use_carry,
2041                    );
2042                }
2043                Step::Lstm {
2044                    x_byte_off,
2045                    w_ih_byte_off,
2046                    w_hh_byte_off,
2047                    bias_byte_off,
2048                    h0_byte_off,
2049                    c0_byte_off,
2050                    dst_byte_off,
2051                    batch,
2052                    seq,
2053                    input_size,
2054                    hidden,
2055                    num_layers,
2056                    bidirectional,
2057                    carry,
2058                } => {
2059                    crate::lstm_host::run_lstm(
2060                        &self.arena,
2061                        &dev.device,
2062                        &dev.queue,
2063                        *x_byte_off as usize,
2064                        *w_ih_byte_off as usize,
2065                        *w_hh_byte_off as usize,
2066                        *bias_byte_off as usize,
2067                        *h0_byte_off as usize,
2068                        *c0_byte_off as usize,
2069                        *dst_byte_off as usize,
2070                        *batch as usize,
2071                        *seq as usize,
2072                        *input_size as usize,
2073                        *hidden as usize,
2074                        *num_layers as usize,
2075                        *bidirectional,
2076                        *carry,
2077                    );
2078                }
2079                Step::ConvTranspose2d {
2080                    src_byte_off,
2081                    weight_byte_off,
2082                    dst_byte_off,
2083                    n,
2084                    c_in,
2085                    h,
2086                    w_in,
2087                    c_out,
2088                    h_out,
2089                    w_out,
2090                    kh,
2091                    kw,
2092                    sh,
2093                    sw,
2094                    ph,
2095                    pw,
2096                    dh,
2097                    dw,
2098                    groups,
2099                } => {
2100                    crate::conv_transpose2d_host::run_conv_transpose2d(
2101                        &self.arena,
2102                        &dev.device,
2103                        &dev.queue,
2104                        *src_byte_off as usize,
2105                        *weight_byte_off as usize,
2106                        *dst_byte_off as usize,
2107                        *n as usize,
2108                        *c_in as usize,
2109                        *h as usize,
2110                        *w_in as usize,
2111                        *c_out as usize,
2112                        *h_out as usize,
2113                        *w_out as usize,
2114                        *kh as usize,
2115                        *kw as usize,
2116                        *sh as usize,
2117                        *sw as usize,
2118                        *ph as usize,
2119                        *pw as usize,
2120                        *dh as usize,
2121                        *dw as usize,
2122                        *groups as usize,
2123                    );
2124                }
2125                Step::GroupNormHost {
2126                    src_byte_off,
2127                    gamma_byte_off,
2128                    beta_byte_off,
2129                    dst_byte_off,
2130                    n,
2131                    c,
2132                    h,
2133                    w,
2134                    num_groups,
2135                    eps,
2136                } => {
2137                    crate::vision_host::run_group_norm(
2138                        &self.arena,
2139                        &dev.device,
2140                        &dev.queue,
2141                        *src_byte_off as usize,
2142                        *gamma_byte_off as usize,
2143                        *beta_byte_off as usize,
2144                        *dst_byte_off as usize,
2145                        *n as usize,
2146                        *c as usize,
2147                        *h as usize,
2148                        *w as usize,
2149                        *num_groups as usize,
2150                        *eps,
2151                    );
2152                }
2153                Step::LayerNorm2dHost {
2154                    src_byte_off,
2155                    gamma_byte_off,
2156                    beta_byte_off,
2157                    dst_byte_off,
2158                    n,
2159                    c,
2160                    h,
2161                    w,
2162                    eps,
2163                } => {
2164                    crate::vision_host::run_layer_norm2d(
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                        *eps,
2177                    );
2178                }
2179                Step::ResizeNearest2xHost {
2180                    src_byte_off,
2181                    dst_byte_off,
2182                    n,
2183                    c,
2184                    h,
2185                    w,
2186                } => {
2187                    crate::vision_host::run_resize_nearest_2x(
2188                        &self.arena,
2189                        &dev.device,
2190                        &dev.queue,
2191                        *src_byte_off as usize,
2192                        *dst_byte_off as usize,
2193                        *n as usize,
2194                        *c as usize,
2195                        *h as usize,
2196                        *w as usize,
2197                    );
2198                }
2199                Step::ReverseHost {
2200                    src_byte_off,
2201                    dst_byte_off,
2202                    dims,
2203                    rev_mask,
2204                    elem_bytes,
2205                } => {
2206                    crate::vision_host::run_reverse(
2207                        &self.arena,
2208                        &dev.device,
2209                        &dev.queue,
2210                        *src_byte_off as usize,
2211                        *dst_byte_off as usize,
2212                        dims,
2213                        rev_mask,
2214                        *elem_bytes as usize,
2215                    );
2216                }
2217                Step::ArgReduceHost {
2218                    src_byte_off,
2219                    dst_byte_off,
2220                    outer,
2221                    reduced,
2222                    inner,
2223                    is_max,
2224                } => {
2225                    crate::vision_host::run_argreduce(
2226                        &self.arena,
2227                        &dev.device,
2228                        &dev.queue,
2229                        *src_byte_off as usize,
2230                        *dst_byte_off as usize,
2231                        *outer as usize,
2232                        *reduced as usize,
2233                        *inner as usize,
2234                        *is_max,
2235                    );
2236                }
2237                Step::GruHost {
2238                    x,
2239                    w_ih,
2240                    w_hh,
2241                    b_ih,
2242                    b_hh,
2243                    h0,
2244                    dst,
2245                    batch,
2246                    seq,
2247                    input_size,
2248                    hidden,
2249                    num_layers,
2250                    bidirectional,
2251                    carry,
2252                } => {
2253                    crate::vision_host::run_gru(
2254                        &self.arena,
2255                        &dev.device,
2256                        &dev.queue,
2257                        *x as usize,
2258                        *w_ih as usize,
2259                        *w_hh as usize,
2260                        *b_ih as usize,
2261                        *b_hh as usize,
2262                        *h0 as usize,
2263                        *dst as usize,
2264                        *batch as usize,
2265                        *seq as usize,
2266                        *input_size as usize,
2267                        *hidden as usize,
2268                        *num_layers as usize,
2269                        *bidirectional,
2270                        *carry,
2271                    );
2272                }
2273                Step::RnnHost {
2274                    x,
2275                    w_ih,
2276                    w_hh,
2277                    bias,
2278                    h0,
2279                    dst,
2280                    batch,
2281                    seq,
2282                    input_size,
2283                    hidden,
2284                    num_layers,
2285                    bidirectional,
2286                    carry,
2287                    relu,
2288                } => {
2289                    crate::vision_host::run_rnn(
2290                        &self.arena,
2291                        &dev.device,
2292                        &dev.queue,
2293                        *x as usize,
2294                        *w_ih as usize,
2295                        *w_hh as usize,
2296                        *bias as usize,
2297                        *h0 as usize,
2298                        *dst as usize,
2299                        *batch as usize,
2300                        *seq as usize,
2301                        *input_size as usize,
2302                        *hidden as usize,
2303                        *num_layers as usize,
2304                        *bidirectional,
2305                        *carry,
2306                        *relu,
2307                    );
2308                }
2309                Step::Llada2GroupLimitedGate {
2310                    sig_byte_off,
2311                    route_byte_off,
2312                    out_byte_off,
2313                    n_elems,
2314                    attrs,
2315                } => {
2316                    crate::llada2_gate_host::run_llada2_group_limited_gate(
2317                        &self.arena,
2318                        &dev.device,
2319                        &dev.queue,
2320                        *sig_byte_off as usize,
2321                        *route_byte_off as usize,
2322                        *out_byte_off as usize,
2323                        *n_elems as usize,
2324                        attrs,
2325                    );
2326                }
2327                Step::UmapKnnHost {
2328                    pairwise_byte_off,
2329                    out_byte_off,
2330                    n,
2331                    k,
2332                } => {
2333                    crate::umap_knn_host::run_umap_knn(
2334                        &self.arena,
2335                        &dev.device,
2336                        &dev.queue,
2337                        *pairwise_byte_off as usize,
2338                        *out_byte_off as usize,
2339                        *n as usize,
2340                        *k as usize,
2341                    );
2342                }
2343                Step::MsDeformAttnHost {
2344                    in_offs,
2345                    out_byte_off,
2346                    out_bytes,
2347                    attrs,
2348                } => {
2349                    crate::ms_deform_attn::run_ms_deform_attn(
2350                        &self.arena,
2351                        &dev.device,
2352                        &dev.queue,
2353                        in_offs,
2354                        *out_byte_off as usize,
2355                        *out_bytes as usize,
2356                        attrs,
2357                    );
2358                }
2359                Step::FftHost {
2360                    src_byte_off,
2361                    dst_byte_off,
2362                    outer,
2363                    n_complex,
2364                    inverse,
2365                    norm_tag,
2366                    dtype_tag,
2367                } => {
2368                    crate::fft_host::run_fft1d(
2369                        &self.arena,
2370                        &dev.device,
2371                        &dev.queue,
2372                        *src_byte_off as usize,
2373                        *dst_byte_off as usize,
2374                        *outer as usize,
2375                        *n_complex as usize,
2376                        *inverse,
2377                        *norm_tag,
2378                        fft_dtype_from_tag(*dtype_tag),
2379                    );
2380                }
2381                Step::ScanHost {
2382                    plan,
2383                    outer_init_off,
2384                    outer_final_off,
2385                    length,
2386                    save_trajectory,
2387                    xs_outer,
2388                    bcast_outer,
2389                } => {
2390                    crate::scan_host::run_scan(
2391                        &self.arena,
2392                        &dev.device,
2393                        &dev.queue,
2394                        plan,
2395                        *outer_init_off,
2396                        *outer_final_off,
2397                        *length,
2398                        *save_trajectory,
2399                        xs_outer,
2400                        bcast_outer,
2401                    );
2402                }
2403                Step::WelchPeaksHost { .. }
2404                | Step::LogMelHost { .. }
2405                | Step::LogMelBackwardHost { .. } => {
2406                    unreachable!("tail-host audio ops run after GPU wait")
2407                }
2408                Step::Im2ColHost {
2409                    x_byte_off,
2410                    col_byte_off,
2411                    n,
2412                    c_in,
2413                    h,
2414                    w,
2415                    h_out,
2416                    w_out,
2417                    kh,
2418                    kw,
2419                    sh,
2420                    sw,
2421                    ph,
2422                    pw,
2423                    dh,
2424                    dw_dil,
2425                } => {
2426                    crate::im2col_host::run_im2col(
2427                        &self.arena,
2428                        &dev.device,
2429                        &dev.queue,
2430                        *x_byte_off as usize,
2431                        *col_byte_off as usize,
2432                        *n,
2433                        *c_in,
2434                        *h,
2435                        *w,
2436                        *h_out,
2437                        *w_out,
2438                        *kh,
2439                        *kw,
2440                        *sh,
2441                        *sw,
2442                        *ph,
2443                        *pw,
2444                        *dh,
2445                        *dw_dil,
2446                    );
2447                }
2448                Step::RngNormalHost {
2449                    dst_byte_off,
2450                    len,
2451                    mean,
2452                    scale,
2453                    key,
2454                    op_seed,
2455                } => {
2456                    let opts = *self.rng.read().expect("rng lock");
2457                    crate::rng_host::run_rng_normal(
2458                        &self.arena,
2459                        &dev.queue,
2460                        *dst_byte_off as usize,
2461                        *len as usize,
2462                        *mean,
2463                        *scale,
2464                        *key,
2465                        *op_seed,
2466                        opts,
2467                    );
2468                }
2469                Step::RngUniformHost {
2470                    dst_byte_off,
2471                    len,
2472                    low,
2473                    high,
2474                    key,
2475                    op_seed,
2476                } => {
2477                    let opts = *self.rng.read().expect("rng lock");
2478                    crate::rng_host::run_rng_uniform(
2479                        &self.arena,
2480                        &dev.queue,
2481                        *dst_byte_off as usize,
2482                        *len as usize,
2483                        *low,
2484                        *high,
2485                        *key,
2486                        *op_seed,
2487                        opts,
2488                    );
2489                }
2490                #[cfg(feature = "splat")]
2491                Step::GaussianSplatRender {
2492                    positions_byte_off,
2493                    positions_len,
2494                    scales_byte_off,
2495                    scales_len,
2496                    rotations_byte_off,
2497                    rotations_len,
2498                    opacities_byte_off,
2499                    opacities_len,
2500                    colors_byte_off,
2501                    colors_len,
2502                    sh_coeffs_byte_off,
2503                    sh_coeffs_len,
2504                    meta_byte_off,
2505                    dst_byte_off,
2506                    dst_len,
2507                    width,
2508                    height,
2509                    tile_size,
2510                    radius_scale,
2511                    alpha_cutoff,
2512                    max_splat_steps,
2513                    transmittance_threshold,
2514                    max_list_entries,
2515                } => {
2516                    crate::splat::run_gaussian_splat_render(
2517                        &self.arena,
2518                        &dev.device,
2519                        &dev.queue,
2520                        *positions_byte_off as usize,
2521                        *positions_len as usize,
2522                        *scales_byte_off as usize,
2523                        *scales_len as usize,
2524                        *rotations_byte_off as usize,
2525                        *rotations_len as usize,
2526                        *opacities_byte_off as usize,
2527                        *opacities_len as usize,
2528                        *colors_byte_off as usize,
2529                        *colors_len as usize,
2530                        *sh_coeffs_byte_off as usize,
2531                        *sh_coeffs_len as usize,
2532                        *meta_byte_off as usize,
2533                        *dst_byte_off as usize,
2534                        *dst_len as usize,
2535                        *width,
2536                        *height,
2537                        *tile_size,
2538                        *radius_scale,
2539                        *alpha_cutoff,
2540                        *max_splat_steps,
2541                        *transmittance_threshold,
2542                        *max_list_entries,
2543                    );
2544                }
2545                #[cfg(feature = "splat")]
2546                Step::GaussianSplatPrepare {
2547                    positions_byte_off,
2548                    positions_len,
2549                    scales_byte_off,
2550                    scales_len,
2551                    rotations_byte_off,
2552                    rotations_len,
2553                    opacities_byte_off,
2554                    opacities_len,
2555                    colors_byte_off,
2556                    colors_len,
2557                    sh_coeffs_byte_off,
2558                    sh_coeffs_len,
2559                    meta_byte_off,
2560                    meta_len,
2561                    prep_byte_off,
2562                    prep_len,
2563                    width,
2564                    height,
2565                    tile_size,
2566                    radius_scale,
2567                    alpha_cutoff,
2568                    max_splat_steps,
2569                    transmittance_threshold,
2570                    max_list_entries,
2571                } => {
2572                    crate::splat::run_gaussian_splat_prepare(
2573                        &self.arena,
2574                        &dev.device,
2575                        &dev.queue,
2576                        *positions_byte_off as usize,
2577                        *positions_len as usize,
2578                        *scales_byte_off as usize,
2579                        *scales_len as usize,
2580                        *rotations_byte_off as usize,
2581                        *rotations_len as usize,
2582                        *opacities_byte_off as usize,
2583                        *opacities_len as usize,
2584                        *colors_byte_off as usize,
2585                        *colors_len as usize,
2586                        *sh_coeffs_byte_off as usize,
2587                        *sh_coeffs_len as usize,
2588                        *meta_byte_off as usize,
2589                        *meta_len as usize,
2590                        *prep_byte_off as usize,
2591                        *prep_len as usize,
2592                        *width,
2593                        *height,
2594                        *tile_size,
2595                        *radius_scale,
2596                        *alpha_cutoff,
2597                        *max_splat_steps,
2598                        *transmittance_threshold,
2599                        *max_list_entries,
2600                    );
2601                }
2602                #[cfg(feature = "splat")]
2603                Step::GaussianSplatRasterize {
2604                    prep_byte_off,
2605                    prep_len,
2606                    meta_byte_off,
2607                    meta_len,
2608                    dst_byte_off,
2609                    dst_len,
2610                    count,
2611                    width,
2612                    height,
2613                    tile_size,
2614                    alpha_cutoff,
2615                    max_splat_steps,
2616                    transmittance_threshold,
2617                    max_list_entries,
2618                } => {
2619                    crate::splat::run_gaussian_splat_rasterize(
2620                        &self.arena,
2621                        &dev.device,
2622                        &dev.queue,
2623                        *prep_byte_off as usize,
2624                        *prep_len as usize,
2625                        *meta_byte_off as usize,
2626                        *meta_len as usize,
2627                        *dst_byte_off as usize,
2628                        *dst_len as usize,
2629                        *count as usize,
2630                        *width,
2631                        *height,
2632                        *tile_size,
2633                        *alpha_cutoff,
2634                        *max_splat_steps,
2635                        *transmittance_threshold,
2636                        *max_list_entries,
2637                    );
2638                }
2639                #[cfg(feature = "splat")]
2640                Step::GaussianSplatRenderBackward {
2641                    positions_byte_off,
2642                    positions_len,
2643                    scales_byte_off,
2644                    scales_len,
2645                    rotations_byte_off,
2646                    rotations_len,
2647                    opacities_byte_off,
2648                    opacities_len,
2649                    colors_byte_off,
2650                    colors_len,
2651                    sh_coeffs_byte_off,
2652                    sh_coeffs_len,
2653                    meta_byte_off,
2654                    d_loss_byte_off,
2655                    d_loss_len,
2656                    packed_byte_off,
2657                    packed_len,
2658                    width,
2659                    height,
2660                    tile_size,
2661                    radius_scale,
2662                    alpha_cutoff,
2663                    max_splat_steps,
2664                    transmittance_threshold,
2665                    max_list_entries,
2666                    loss_grad_clip,
2667                    sh_band,
2668                    max_anisotropy,
2669                } => {
2670                    crate::splat::run_gaussian_splat_render_backward(
2671                        &self.arena,
2672                        &dev.device,
2673                        &dev.queue,
2674                        *positions_byte_off as usize,
2675                        *positions_len as usize,
2676                        *scales_byte_off as usize,
2677                        *scales_len as usize,
2678                        *rotations_byte_off as usize,
2679                        *rotations_len as usize,
2680                        *opacities_byte_off as usize,
2681                        *opacities_len as usize,
2682                        *colors_byte_off as usize,
2683                        *colors_len as usize,
2684                        *sh_coeffs_byte_off as usize,
2685                        *sh_coeffs_len as usize,
2686                        *meta_byte_off as usize,
2687                        *d_loss_byte_off as usize,
2688                        *d_loss_len as usize,
2689                        *packed_byte_off as usize,
2690                        *packed_len as usize,
2691                        *width,
2692                        *height,
2693                        *tile_size,
2694                        *radius_scale,
2695                        *alpha_cutoff,
2696                        *max_splat_steps,
2697                        *transmittance_threshold,
2698                        *max_list_entries,
2699                        *loss_grad_clip,
2700                        *sh_band,
2701                        *max_anisotropy,
2702                    );
2703                }
2704                _ => break,
2705            }
2706            step_i += 1;
2707        }
2708
2709        self.dump_node_stats_if_requested(dev);
2710
2711        if rlx_ir::env::flag("RLX_WGPU_NAN_TRACE") {
2712            let mut bad_nodes = Vec::new();
2713            for node in self.graph.nodes() {
2714                if !self.arena.has(node.id) {
2715                    continue;
2716                }
2717                // Skip leaves — populated by host writes, not kernels.
2718                if matches!(
2719                    node.op,
2720                    rlx_ir::Op::Input { .. }
2721                        | rlx_ir::Op::Param { .. }
2722                        | rlx_ir::Op::Constant { .. }
2723                ) {
2724                    continue;
2725                }
2726                let data = self.arena.read_f32(&dev.device, &dev.queue, node.id);
2727                let nan_count = data.iter().filter(|v| v.is_nan()).count();
2728                let inf_count = data.iter().filter(|v| v.is_infinite()).count();
2729                if nan_count > 0 || inf_count > 0 {
2730                    // Capture first NaN index + the values around it.
2731                    let first_nan = data.iter().position(|v| v.is_nan());
2732                    if let Some(idx) = first_nan {
2733                        let lo = idx.saturating_sub(2);
2734                        let hi = (idx + 3).min(data.len());
2735                        eprintln!(
2736                            "  node {:?} op={:?} len={} nan={} inf={} \
2737                                   first_nan_idx={} ctx={:?}",
2738                            node.id,
2739                            node.op,
2740                            data.len(),
2741                            nan_count,
2742                            inf_count,
2743                            idx,
2744                            &data[lo..hi]
2745                        );
2746                    }
2747                    bad_nodes.push((node.id, data.len(), nan_count, inf_count));
2748                    if bad_nodes.len() >= 3 {
2749                        break;
2750                    }
2751                }
2752            }
2753            if bad_nodes.is_empty() {
2754                eprintln!("[wgpu-nan-trace] no NaN/Inf in any node — clean run");
2755            } else {
2756                eprintln!(
2757                    "[wgpu-nan-trace] first {} bad nodes (above)",
2758                    bad_nodes.len()
2759                );
2760            }
2761        }
2762
2763        if rlx_ir::env::flag("RLX_BENCH_DISPATCH_ONLY") {
2764            return self
2765                .graph
2766                .outputs
2767                .iter()
2768                .map(|&id| {
2769                    let n = self.graph.node(id).shape.num_elements().unwrap_or(0);
2770                    vec![0.0; n]
2771                })
2772                .collect();
2773        }
2774        let out_ids: Vec<_> = self.graph.outputs.clone();
2775        read_f32_many_pooled(
2776            &self.arena,
2777            &dev.device,
2778            &dev.queue,
2779            &out_ids,
2780            &mut self.readback_staging,
2781        )
2782    }
2783}