[ExecuTorch][WebGPU] Promote unary op plumbing to a shared 2D-dispatch helper#21297
Conversation
…h helper Pull Request resolved: #21153 Prep for porting the Vulkan delegate's unary activations. The elementwise-unary plumbing was file-local to the `sigmoid` op, so each new activation would duplicate it. This promotes it to a shared `runtime/ops/unary/UnaryOp.{h,cpp}` helper `add_unary_op(...)` that takes `min`/`max` params (dummy for no-param ops), mirroring the Vulkan `add_unary_op_node` (`backends/vulkan/runtime/graph/ops/impl/UnaryOp.cpp:36`), and migrates the landed `sigmoid` op onto it. The helper also adopts the 2D-fold dispatch the `add` op already uses, so unary ops on large tensors don't hit the 65535 workgroups-per-dimension cap. Key changes: - `runtime/ops/unary/UnaryOp.{h,cpp}` — shared `add_unary_op`: null/fp32/size guards (throw, never silent), a `UnaryParams{num_elements,min,max}` uniform mirroring Vulkan's `min_max` push constant, `compute_2d_workgroup_count` dispatch, and a resize hook that rewrites the full params (including `min`/`max`) and both dispatch dims. - `runtime/ops/sigmoid/UnaryOp.cpp` — includes the shared header and calls `add_unary_op`; the file-local copy is removed. - `runtime/ops/sigmoid/sigmoid.wgsl` (+ regenerated `sigmoid_wgsl.h`) — migrated to the 2D index idiom (`gid.x + gid.y*(num_workgroups.x*wg_size)`) and `wg_size=256`, matching `binary_add.wgsl`. - `CMakeLists.txt` — add `runtime/ops/unary/UnaryOp.cpp` to `WEBGPU_SRCS`. `sigmoid` output is unchanged: for element counts within the per-dim cap the 2D index reduces to the old 1D `gid.x`, and the map body is identical. ghstack-source-id: 406360825 @exported-using-ghexport Differential Revision: [D112257655](https://our.internmc.facebook.com/intern/diff/D112257655/)
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21297
Note: Links to docs will display an error until the docs builds have been completed. ❌ 1 New Failure, 41 Unrelated FailuresAs of commit 10756aa with merge base e2c9a48 ( NEW FAILURE - The following job has failed:
FLAKY - The following jobs failed but were likely due to flakiness present on trunk:
BROKEN TRUNK - The following jobs failed but were present on the merge base:👉 Rebase onto the `viable/strict` branch to avoid these failures
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
This PR needs a
|
Pull Request resolved: #21154 Ports the Vulkan delegate's no-param unary activations — `abs`, `exp`, `sqrt`, `rsqrt`, `sin`, `cos`, `tanh`, `round`, `neg`, `hardswish` — built on the shared `add_unary_op` helper. Each is a per-op `.wgsl` (2D-fold dispatch, `wg_size=256`) whose body mirrors the Vulkan formula in `backends/vulkan/runtime/graph/ops/glsl/unary_op.yaml` + `activations.h` verbatim, registered through `runtime/ops/unary/Activations.cpp`. Key changes: - `runtime/ops/unary/{abs,exp,sqrt,rsqrt,sin,cos,tanh,round,neg,hardswish}.wgsl` (+ generated `_wgsl.h`) — `output[idx] = <vulkan-math>`; notably `rsqrt` = `1.0 / sqrt(x)`, `tanh` = `tanh(clamp(x, -15.0, 15.0))`, `hardswish` = the piecewise `select`. - `runtime/ops/unary/Activations.cpp` — registers `aten.<op>.default` for the 10 ops, each calling the shared `add_unary_op`. - `CMakeLists.txt` — `runtime/ops/unary/Activations.cpp` in `WEBGPU_SRCS`. `tan` and `hardsigmoid` are intentionally excluded: the Vulkan partitioner (`backends/vulkan/op_registry.py`) does not tag them, so they cannot be delegated to this backend without a separate partitioner change. ghstack-source-id: 406360832 @exported-using-ghexport Differential Revision: [D112257637](https://our.internmc.facebook.com/intern/diff/D112257637/)
Pull Request resolved: #21155 Adds the declarative op-test suites for the unary activations ported in the diff below. Each op gets a `torch` reference plus a deterministic, range-bounded input so the fp64 golden is well-defined — positive for `sqrt`/`rsqrt`, spanning the ±3 knees for `hardswish`, and reaching the ±15 clamp for `tanh` — driving the manifest-based Dawn golden. Key changes: - `test/ops/test_unary_activations.py` — `UnaryModule` + `UNARY_G1` (op name -> torch fn + linspace input gen). - `test/op_tests/cases.py` — registers the 10 suites (a `mat` + a `rank3` case each) via a factory loop, mirroring the existing per-op registration convention. ghstack-source-id: 406360831 @exported-using-ghexport Differential Revision: [D112257657](https://our.internmc.facebook.com/intern/diff/D112257657/)
Pull Request resolved: #21156 Ports the Vulkan delegate's clamp-family activations. `clamp` and `hardtanh` share one `clamp(x, min, max)` shader (mirroring how Vulkan reuses its clamp shader for both), built on the shared unary helper. Bounds come from the op's scalar args, with `None -> +/-inf` matching Vulkan `get_val_or_inf` (`backends/vulkan/runtime/graph/ops/impl/UnaryOp.cpp:72`). Key changes: - `runtime/ops/unary/clamp.wgsl` (+ generated `_wgsl.h`) — `output[idx] = clamp(input[idx], params.minimum, params.maximum)`; the `minimum`/`maximum` member names mirror Vulkan `unary_op.glsl`. - `runtime/ops/unary/Activations.cpp` — `clamp_impl` / `hardtanh_impl` (args `[in, min, max, out]`) + `get_val_or_inf` (reads an Int/Double scalar, substitutes `+/-inf` for `None`, throws on any other type). - `runtime/ops/unary/UnaryOp.cpp` — the shared helper now rejects int operands (fail-loud): the fp32-only backend would otherwise reinterpret int bits as f32, and `clamp` is the one unary the Vulkan partitioner (`op_registry.py`) tags for integer tensors. `relu6` is excluded: the Vulkan partitioner does not tag it, so it cannot be delegated without a separate partitioner change. ghstack-source-id: 406360830 @exported-using-ghexport Differential Revision: [D112257672](https://our.internmc.facebook.com/intern/diff/D112257672/)
Pull Request resolved: #21157 Adds the op-test suites for the clamp-family ops ported in the diff below. `clamp` covers both-bounds and a `min=None` case (exercising the `None -> -inf` substitution); `hardtanh` covers the default `(-1, 1)` bounds and a wider `(-2, 2)`. Key changes: - `test/ops/test_unary_activations.py` — `ClampModule`, `HardtanhModule`, `CLAMP_CONFIGS`, `HARDTANH_CONFIGS`. - `test/op_tests/cases.py` — `_clamp_suite` + `_hardtanh_suite`. ghstack-source-id: 406360836 @exported-using-ghexport Differential Revision: [D112257631](https://our.internmc.facebook.com/intern/diff/D112257631/)
Pull Request resolved: #21158 Ports the Vulkan delegate's `minimum` with NumPy broadcasting: `out = min(in1, in2)`, mirroring the sibling `mul` op + Vulkan `binary_op_buffer` (3 per-tensor `TensorMeta` uniforms; size-1 input dims clamped and the flat output index relinearized; equal-shape fast path). Key changes: - `runtime/ops/minimum/{BinaryOp.cpp, binary_minimum.wgsl}` (+ generated `_wgsl.h`) — `minimum_impl` (args `[in1, in2, out]`); broadcast `TensorMeta` path + dual-operand resize hook (mirrors `mul`); fail-loud int guard (the fp32-only backend would otherwise reinterpret int bits as f32). - `CMakeLists.txt` — `runtime/ops/minimum/BinaryOp.cpp` in `WEBGPU_SRCS`. Note: WGSL `min` and `torch.minimum` differ on NaN, matching the Vulkan reference; the golden uses finite inputs. ghstack-source-id: 406360835 @exported-using-ghexport Differential Revision: [D112257607](https://our.internmc.facebook.com/intern/diff/D112257607/)
Pull Request resolved: #21159 Adds the minimum op-test suite (2d + 3d same-shape) for the op ported in the diff below. Key changes: - `test/ops/test_minimum.py` — `MinimumModule` (`torch.minimum`). - `test/op_tests/cases.py` — `_minimum_suite`. ghstack-source-id: 406360846 @exported-using-ghexport Differential Revision: [D112257590](https://our.internmc.facebook.com/intern/diff/D112257590/)
Pull Request resolved: #21160 Ports `pow.Tensor_Tensor` with NumPy broadcasting, mirroring the sibling `mul` op + Vulkan `binary_op_buffer` (per-tensor `TensorMeta` uniforms; size-1 input dims clamped and the flat output index relinearized; equal-shape fast path). Key changes: - `runtime/ops/pow/{BinaryOp.cpp, binary_pow.wgsl}` (+ generated `_wgsl.h`) — `pow_impl` (args `[in1, in2, out]`); broadcast `TensorMeta` path + dual-operand resize hook; fail-loud int guard (fp32-only backend). Note: WGSL `pow(x, y)` is undefined for `x < 0`, exact parity with the Vulkan GLSL `pow`. Co-authored-with: Claude Code. ghstack-source-id: 406360857 @exported-using-ghexport Differential Revision: [D112257673](https://our.internmc.facebook.com/intern/diff/D112257673/)
Pull Request resolved: #21161 Adds the op-test suite for `pow.Tensor_Tensor` (ported in the diff below). Uses a strictly-positive base so `pow(neg, frac)=NaN` is never exercised. Key changes: - `test/ops/test_pow.py` + `test/op_tests/cases.py` — `PowModule` + `_pow_suite` (2d + 3d equal-shape, `atol=rtol=1e-3` vs the `torch` golden). Co-authored-with: Claude Code. ghstack-source-id: 406360852 @exported-using-ghexport Differential Revision: [D112257639](https://our.internmc.facebook.com/intern/diff/D112257639/)
Pull Request resolved: #21162 Ports `pow.Tensor_Scalar` (tensor ^ scalar-exponent) on the shared unary helper: the exponent rides the `min` uniform slot, exactly like `leaky_relu`'s slope. `output[idx] = pow(input[idx], exponent)`. Key changes: - `runtime/ops/unary/pow_scalar.wgsl` (+ generated `_wgsl.h`) — `pow(input[idx], params.minimum)`. - `runtime/ops/unary/Activations.cpp` — `pow_scalar_impl` (args `[in, exponent, out]`; exponent read via the shared `get_val_or_inf` into the min slot), registered `aten.pow.Tensor_Scalar`. Inherits the shared helper's fp32 int-guard + resize hook (the exponent is re-applied on resize). Note: WGSL `pow` returns NaN for a negative base at any exponent (matching the reference GPU `pow`); the suite uses a positive base. ghstack-source-id: 406360862 @exported-using-ghexport Differential Revision: [D112257604](https://our.internmc.facebook.com/intern/diff/D112257604/)
Pull Request resolved: #21163 Adds the pow_scalar op-test suite (square + sqrt exponents, positive base) for the op ported in the diff below. Key changes: - `test/ops/test_unary_activations.py` — `PowScalarModule` + `POW_SCALAR_CONFIGS`. - `test/op_tests/cases.py` — `_pow_scalar_suite`. ghstack-source-id: 406360849 @exported-using-ghexport Differential Revision: [D112257612](https://our.internmc.facebook.com/intern/diff/D112257612/)
Pull Request resolved: #21164 Ports the Vulkan delegate's `amax`/`amin` as last-dim per-row reductions — the buffer-storage path Vulkan itself restricts reductions to (`impl/Reduce.cpp` `add_reduce_per_row_node`; its texture path throws on buffer storage). One thread per output row serially reduces over the contiguous last dim. Key changes: - `runtime/ops/amax/{Reduce.cpp, amax.wgsl}` + `runtime/ops/amin/{Reduce.cpp, amin.wgsl}` (+ generated `_wgsl.h`) — `out[row] = reduce(input[row*W .. +W])` where `W` = last-dim extent; args `[in, dim, keepdim, out]`; fp32 int-guard; keepdim-aware resize hook; registered `aten.amax.default` / `aten.amin.default`. - `CMakeLists.txt` — both in `WEBGPU_SRCS`. Constraint: only a single last-dim reduction (`dim == -1` or `ndim-1`) is supported on buffer storage; any other reduce dim throws (mirrors Vulkan's buffer-per-row gate). The accumulator seeds `input[base]` (not 0), so all-negative rows reduce correctly. ghstack-source-id: 406360850 @exported-using-ghexport Differential Revision: [D112257619](https://our.internmc.facebook.com/intern/diff/D112257619/)
Pull Request resolved: #21165 Adds op-test suites for the last-dim reductions ported in the diff below (keepdim + nodim, 2d + 3d). Key changes: - `test/ops/test_reduce.py` — `AmaxModule` / `AminModule` (reduce `dim=-1`). - `test/op_tests/cases.py` — `_reduce_suite` + `_amax_suite` / `_amin_suite`. ghstack-source-id: 406360856 @exported-using-ghexport Differential Revision: [D112257621](https://our.internmc.facebook.com/intern/diff/D112257621/)
Pull Request resolved: #21166 Ports `aten.flip.default` as a per-element index-transform, mirroring the landed `permute` op (same dual-`TensorMeta` + 5-binding bind group). flip is pure data movement, so it reverses the coord along each flagged dim and copies — no fp compute. Key changes: - `runtime/ops/flip/{Flip.cpp, flip.wgsl}` (+ generated `flip_wgsl.h`) — per out flat idx: unravel via `out_meta.strides`, `in_coord = select(coord, sizes[d]-1-coord, flip[d]==1)`, ravel via `in_meta.strides`, `output[out] = input[in]`; `Params { flip: vec4<u32> }` bitmap built from the normalized `dims` arg; registered `aten.flip.default`. - `CMakeLists.txt` — `runtime/ops/flip/Flip.cpp` in `WEBGPU_SRCS`. Matches Vulkan `impl/Flip.cpp` (`create_whcn_bitmap` normalizes/flags dims; reversal `size-1-coord`). No int dtype-guard (unlike clamp/minimum which do fp arithmetic): flip is bit-preserving movement, so any 4-byte dtype is correct; the `nbytes != numel*4` guard rejects int64/fp16. 2D-folded dispatch lifts the 65535 cap. `out` is the last arg (this backend's convention). ghstack-source-id: 406360854 @exported-using-ghexport Differential Revision: [D112257643](https://our.internmc.facebook.com/intern/diff/D112257643/)
Pull Request resolved: #21167 Adds the op-test suite for the `flip` op ported in the diff below. Key changes: - `test/ops/test_flip.py` — `FlipModule` (reverses `dims`) + `FlipTest` export-delegation smoke test (mirrors `PermuteTest`). - `test/op_tests/cases.py` — `_flip_suite`: last `[-1]`/2d, dim0 `[0]`/2d, both_3d `[0,2]`/3d, mid_3d `[1]`/3d, multi_4d `[1,3]`/4d. ghstack-source-id: 406360858 @exported-using-ghexport Differential Revision: [D112257622](https://our.internmc.facebook.com/intern/diff/D112257622/)
Pull Request resolved: #21168 Ports `aten.repeat.default` as a per-element tiling gather, mirroring the landed `permute`/`flip` index-transform (dual-`TensorMeta`, 2D-fold dispatch). repeat is pure data movement, so it maps each output element to its source via a per-dim modulo and copies — no fp compute. Key changes: - `runtime/ops/repeat/{Repeat.cpp, repeat.wgsl}` (+ generated `repeat_wgsl.h`) — per out flat idx: unravel via `out_meta.strides`, `in_coord = out_coord % in_meta.sizes[in_d]`, ravel via `in_meta.strides`, `output[out] = input[in]`; registered `aten.repeat.default`. - `CMakeLists.txt` — `runtime/ops/repeat/Repeat.cpp` in `WEBGPU_SRCS`. Mirrors Vulkan `glsl/repeat_buffer.glsl` (`in_tidx[d] = out_tidx[d] % size_at(in_meta, d)`). Vulkan's innermost-first `BufferMetadata` auto-aligns a shorter input; WebGPU's NCHW `TensorMeta` makes the right-alignment explicit via `offset = out_ndim - in_ndim` (leading `d < offset` dims are pure prepends, contributing 0). No `repeats`-arg read needed: the repeated out dims are baked in by export, so the tiling is recoverable from shapes alone. No int dtype-guard (bit-preserving movement, like `flip`); the `nbytes != numel*4` guard rejects int64/fp16. ghstack-source-id: 406360860 @exported-using-ghexport Differential Revision: [D112257679](https://our.internmc.facebook.com/intern/diff/D112257679/)
Pull Request resolved: #21169 Adds the op-test suite for the `repeat` op ported in the diff below. Key changes: - `test/ops/test_repeat.py` — `RepeatModule` (`x.repeat(*repeats)`) + `RepeatTest` export-delegation smoke test (mirrors `PermuteTest`/`FlipTest`). - `test/op_tests/cases.py` — `_repeat_suite`: tile_1d, tile_2d, prepend_3d `[1,3,2]`, prepend_ext `[2,3,1]`, tile_3d `[2,1,2]`. ghstack-source-id: 406360847 @exported-using-ghexport Differential Revision: [D112257616](https://our.internmc.facebook.com/intern/diff/D112257616/)
Pull Request resolved: #21170 Extends the op-test generator to golden-verify EVERY output of a multi-output op (e.g. `split_with_sizes_copy` → N tensors), not just `output[0]`. The C++ driver already selected `outs[output_index]` per manifest entry; only the generator hardcoded a single `output_index = 0`. Key changes: - `test/op_tests/generate_op_tests.py` — `generate_case` now returns one manifest entry PER output tensor: it iterates the module's tuple return, writes a golden per output (`{case}_out{i}.golden.bin`, `output_index = i`), and runs the dual-oracle gate per output. `generate()` uses `extend`. Single-output ops emit exactly one entry, byte-identical to before (no `_out` suffix, `output_index = 0`). - `test/op_tests/test_generator.py` — `test_generate_case_writes_artifacts` reads the returned list (asserts one entry for the single-output `add`). ghstack-source-id: 406360853 @exported-using-ghexport Differential Revision: [D112257608](https://our.internmc.facebook.com/intern/diff/D112257608/)
Pull Request resolved: #21171 Ports `aten.index_select.default` as a gather along a dim, mirroring the landed `flip`/`split` index-transform (dual-`TensorMeta`, 2D-fold dispatch) + the landed `index/Index.cpp` int-index binding. index_select is pure data movement, so it remaps the dim coord through an int index and copies — no fp compute. Key changes: - `runtime/ops/index_select/{IndexSelect.cpp, index_select.wgsl}` (+ generated `index_select_wgsl.h`) — per out flat idx: unravel via `out_meta.strides`, `in_coord = out_coord` with the dim coord remapped by `index[out_coord[dim]]`, ravel via `in_meta.strides`, `output[out] = input[in]`; the index tensor is bound as `array<i32>` (the int32 `downcast_64_bit` of the int64 index, same as `Index.cpp`); `Params { info: vec4<u32> }` carries `dim`. Registered `aten.index_select.default`. - `CMakeLists.txt` — `runtime/ops/index_select/IndexSelect.cpp` in `WEBGPU_SRCS`. Buffer re-derivation of the gather (Vulkan `IndexSelect.cpp` is texture/channel-packed only — `op_registry.py` tags it `CHANNELS_PACKED_TEXTURE`; this backend is buffer-only, so the NCHW index math is derived and CPU-de-risked, cf. `split.wgsl`). Index-value bounds are trusted/unclamped, matching Vulkan `index_select.glsl`; a negative index is a memory-safe WGSL OOB read (no trap), same safety class as Vulkan's clamped `texelFetch`. No int dtype-guard on self/out (bit-preserving); `nbytes` guards reject non-fp32 self/out and a non-int32 index. ghstack-source-id: 406360861 @exported-using-ghexport Differential Revision: [D112257660](https://our.internmc.facebook.com/intern/diff/D112257660/)
Pull Request resolved: #21172 Adds the op-test suite for the `index_select` op ported in the diff below. Key changes: - `test/ops/test_index_select.py` — `IndexSelectModule` gathers via a baked int index buffer, so the only runtime input is the float tensor (the op-test framework is float-only); `torch.index_select(x, dim, self.index)`. + `IndexSelectTest` delegation smoke test (mirrors `SplitTest`). - `test/op_tests/cases.py` — `_index_select_suite`: dim0_1d, dim0_2d, dim1_2d (repeated index), dim1_3d (middle dim), dim2_3d. ghstack-source-id: 406360855 @exported-using-ghexport Differential Revision: [D112257648](https://our.internmc.facebook.com/intern/diff/D112257648/)
Pull Request resolved: #21173 Ports `aten.native_group_norm.default` as a 2-pass, 3-output group normalization, the second Phase B (vision) op. Pass 1 reduces per group to `mean`/`rstd`; pass 2 applies the per-channel affine. Key changes: - `runtime/ops/native_group_norm/{GroupNorm.cpp, group_norm_reduce.wgsl, group_norm.wgsl}` (+ generated `_wgsl.h`) — reduce: one thread per `(n, group)` serially scans that group's contiguous NCHW block (`base = n*C*HxW + g*(C/G)*HxW`, `group_size = (C/G)*HxW` elements), writing `mean = s/count`, `rstd = inverseSqrt(ss/count - mean^2 + eps)`; normalize: one thread per element, `g = c / (C/G)`, `out = (x - mean[n*G+g]) * rstd[n*G+g] * weight[c] + bias[c]`. A file-local `add_gn_dispatch` helper builds each of the two dispatches from an explicit binding list sharing one params UBO. Registered `aten.native_group_norm.default`. - `CMakeLists.txt` — `runtime/ops/native_group_norm/GroupNorm.cpp` in `WEBGPU_SRCS`. Mirrors Vulkan `impl/GroupNorm.cpp` (2 nodes: reduce -> mean/rstd, then normalize) + `glsl/group_norm_reduce_texture.glsl` (`mean=sum/count`, `variance=sumsq/count - mean^2`, `rstd=1/sqrt(var+eps)`). Buffer NCHW re-derivation (Vulkan is texture/channels-packed), so a serial per-group reduce (a group's channels x HxW are contiguous in NCHW) replaces the cooperative texture reduction. All 3 outputs are real. The inter-pass RAW ordering (normalize reads the reduce's mean/rstd) is guaranteed because `execute()` runs one compute pass per dispatch (`WebGPUGraph.cpp`; proven by `test_dispatch_order.cpp` to 1M elems). A dynamic-shape resize hook recomputes params + both dispatch counts + propagates cur_dims for out/mean/rstd. Fail-loud guards: 4D, `C % group == 0`, fp32, weight/bias len `== C`, mean/rstd size `== N*group`. ghstack-source-id: 406360859 @exported-using-ghexport Differential Revision: [D112257596](https://our.internmc.facebook.com/intern/diff/D112257596/)
Pull Request resolved: #21174 Adds the op-test suite for the `native_group_norm` op ported in the diff below, using the multi-output golden support to verify all 3 outputs. Key changes: - `test/ops/test_group_norm.py` — `GroupNormModule` returns the full `torch.native_group_norm` (out, mean, rstd) tuple, so the multi-output golden path checks both the reduce (mean/rstd) and normalize (out) passes. + `GroupNormTest` delegation smoke test. - `test/op_tests/cases.py` — `_group_norm_suite`: c4_g2, c6_g3, c4_g1. ghstack-source-id: 406360848 @exported-using-ghexport Differential Revision: [D112257626](https://our.internmc.facebook.com/intern/diff/D112257626/)
Pull Request resolved: #21175 Ports `aten.avg_pool2d.default` as a windowed spatial average, a Phase B (vision) op. Each output cell averages its KxK input window. Key changes: - `runtime/ops/avg_pool2d/{AvgPool2d.cpp, avg_pool2d.wgsl}` (+ generated `avg_pool2d_wgsl.h`) — per NCHW output element: `ipos = out*stride - padding`, window `[max(0,ipos), min(ipos+kernel, in))`, `acc = sum(window)`, `out = acc / divisor`. The divisor mirrors the three ATen cases: `divisor_override` if given, else `count_include_pad` counts the padded window minus any overhang past the padded edge, else the actual number of summed cells. Registered `aten.avg_pool2d.default`. - `CMakeLists.txt` — `runtime/ops/avg_pool2d/AvgPool2d.cpp` in `WEBGPU_SRCS`. Mirrors Vulkan `glsl/avg_pool2d.glsl` (`ipos`/`start`/`end`/`sum`/`empty`-overhang divisor) + `impl/Pool.cpp` (arg order, `divisor_override` read only when Int, `count_include_pad` bool). Buffer NCHW re-derivation (Vulkan is texture/channels-packed). `read_pair` normalizes each IntList param (empty stride -> kernel, len-1 broadcast, len-2). A dynamic-shape resize hook recomputes the pooled output extent via `pool_out_dim` (floor, or ceil + last-window-drop for `ceil_mode`) + params + dispatch + cur_dims. Fail-loud guards: 4D, fp32. ghstack-source-id: 406360851 @exported-using-ghexport Differential Revision: [D112257592](https://our.internmc.facebook.com/intern/diff/D112257592/)
Pull Request resolved: #21176 Adds the op-test suite for the `avg_pool2d` op ported in the diff below. Key changes: - `test/ops/test_avg_pool2d.py` — `AvgPool2dModule` (`F.avg_pool2d`, parametrized by kernel/stride/padding/count_include_pad/ceil_mode/divisor) + `AvgPool2dTest` delegation smoke test. - `test/op_tests/cases.py` — `_avg_pool2d_suite`: basic, pad_cip, pad_nocip, asym, divisor, ceil_cip, ceil_nocip. ghstack-source-id: 406366786 @exported-using-ghexport Differential Revision: [D113493800](https://our.internmc.facebook.com/intern/diff/D113493800/)
Pull Request resolved: #21215 Problem: The WebGPU delegate has no `et_vk.grid_priors` — a detection anchor-grid op (generates per-cell coordinate shifts). It delegates through `VulkanPartitioner` (`op_registry.py` tags `et_vk.grid_priors.default`; reachable via a direct custom-op call) but had no WebGPU handler. Solution: Port `et_vk.grid_priors` (fp32), mirroring Vulkan `impl/GridPriors.cpp` + `glsl/grid_priors.glsl`. The output is `[H*W, 2]` computed purely from the input's H/W plus scalar `stride`/`offset` — the input's VALUES are not read. Per output row `r`: `out[r,0] = (r%W + offset) * stride` (x-shift), `out[r,1] = (r/W + offset) * stride` (y-shift), matching the Vulkan glsl (`pos.x==0 -> pos.y%width`, `pos.x==1 -> pos.y/width`) and the CPU eager (`custom_ops_lib.py` grid_priors: `stack((shift_x_per_col, shift_y_per_row))`). Implementation: `grid_priors/GridPriors.cpp` registers `et_vk.grid_priors.default` -> `grid_priors_impl`. Args `[in, stride(int), offset(float), out]`; only `in.dims[-2:]` (H, W) are used. `grid_priors.wgsl`: one thread per output element, `row=idx/2`, even idx -> `row%W`, odd -> `row/W`, `(coord+offset)*stride`; 2D-folded dispatch. `GridPriorsParams` (16 bytes: numel/width/stride/offset) matches the WGSL Params. Only the output (storage) + params (uniform) are bound — the input's data is intentionally unread. Resize hook recomputes numel/width + dispatch + out dims `[H*W, 2]`. Guards: in >= 2D, W>0, numel<=u32, fp32 out, all fail-loud. Constraints / divergences from the Vulkan reference: buffer re-derivation of Vulkan's texture (`imageStore`/`VEC4_T`) kernel — the flat-index buffer form is forced by the backend's buffer-only design. `stride` (int) is promoted to f32 for the multiply (exact for detection-scale strides; matches the glsl's int->float promotion). ghstack-source-id: 406366861 @exported-using-ghexport Differential Revision: [D112257635](https://our.internmc.facebook.com/intern/diff/D112257635/)
Pull Request resolved: #21216 Problem: The new `et_vk.grid_priors` op needs golden coverage — and, since the input's values are unused (only its H/W matter), a test that validates the computed grid + the column ordering across shapes. Solution: `GridPriorsModule` calls the custom op directly with baked `stride`/`offset`, so only the float tensor `x` is a runtime input. The op has a CPU eager impl (an independent meshgrid/stack reference), so the op-test framework goldens the WebGPU output against it directly (float32 oracle — the output is an exact computed grid). Non-square shapes (8x10, 5x7) make the two coordinate ranges differ, so a swapped `r%W`/`r/W` column mapping would diverge from the golden. Implementation: `cases.py` registers `grid_priors` with `s8` (8x10, stride=8, offset=0.5), `s16` (4x4, stride=16, offset=0), `offset0` (5x7, stride=4, offset=0); `test_grid_priors.py` holds the module + a delegation smoke test asserting `et_vk.grid_priors` is absorbed into the VulkanBackend delegate. ghstack-source-id: 406366867 @exported-using-ghexport Differential Revision: [D112257599](https://our.internmc.facebook.com/intern/diff/D112257599/)
Pull Request resolved: #21217 Problem: The WebGPU delegate has no `et_vk.conv_with_clamp` — a fused fp32 2D convolution + output clamp (e.g. a `Conv2d` followed by `relu6`). It delegates through `VulkanPartitioner` (`op_registry.py` tags `et_vk.conv_with_clamp.default`; `nn.Conv2d`+`F.relu6` fuses to it) but had no WebGPU handler. Solution: Port `et_vk.conv_with_clamp` (fp32, groups==1), mirroring the Vulkan `conv` handler (`impl/Convolution.cpp:837`) + the eager `clamp(convolution(...), output_min, output_max)` (`custom_ops_lib.py`). Direct windowed conv reusing the staged `q8ta_conv2d` windowing structure but fp32 (no int8 unpack/dequant/requant) + a clamp epilogue: per NCHW output cell `out[n,oc,oh,ow] = clamp(bias[oc] + Σ_{ic,kh,kw} weight[oc,ic,kh,kw] * input[n,ic, oh*sh-ph+kh*dh, ow*sw-pw+kw*dw], output_min, output_max)`, OOB taps skipped. Implementation: `conv_with_clamp/ConvWithClamp.cpp` registers `et_vk.conv_with_clamp.default` -> `conv_with_clamp_impl`. Args `[in, weight, bias?, stride, padding, dilation, transposed, output_padding, groups, output_min?, output_max?, out]`; stride/padding/dilation as int-pairs, `output_min`/`output_max` as `Scalar?` (Double/Int -> value, absent -> -/+inf so `clamp` is a passthrough). `conv_with_clamp.wgsl`: one thread per output element, RAW `[OC,IC,Kh,Kw]` weight (the fp32 buffer path gets the raw constant, like conv1d_dw/pw — not the q8ta im2col `[OC,Kh*Kw*IC]`), 2D-folded dispatch. `ConvWithClampParams` (80 bytes) matches the WGSL Params. Guards: in/weight/out 4D + fp32, `weight.dims[1]==IC`, numel<=u32, groups==1 + not-transposed, all fail-loud. Constraints / divergences from the Vulkan reference: buffer re-derivation of Vulkan's texture conv (`conv2d_prepack_weights.glsl`) — the naive one-thread-per-output form is forced by the buffer-only backend. Scoped to `groups==1` + not-transposed (fail-loud otherwise). Note: overlaps the general `convolution` op — this handles the clamped variant only. ghstack-source-id: 406366872 @exported-using-ghexport Differential Revision: [D112257644](https://our.internmc.facebook.com/intern/diff/D112257644/)
Pull Request resolved: #21218 Problem: The new `et_vk.conv_with_clamp` op (a first-of-its-kind general fp32 conv2d) needs golden coverage that exercises the H/W axis separation, not just square/symmetric configs. Solution: `ConvWithClampModule` is a plain `nn.Conv2d` + `F.relu6`, which the Vulkan fusion rewrites to `et_vk.conv_with_clamp` (fp32 conv + clamp[0,6]); the conv weight/bias are baked params and only `x` is a runtime input. Goldened vs the module's fp32 eager. The `asym` case is fully axis-asymmetric (input H=7≠W=9, kernel Kh=2≠Kw=3, stride 1≠2, padding 1≠0, dilation 2≠1 → output H_out=7≠W_out=4), so an H↔W index swap anywhere (unravel divisors, stride/pad/dilation axis, or Kh/Kw) diverges from the golden. Implementation: `cases.py` registers `conv_with_clamp` with `k3p1`/`stride2`/`dil2`/`no_bias`/`asym` (all groups==1); `test_conv_with_clamp.py` holds the module + a delegation smoke test asserting `et_vk.conv_with_clamp` is absorbed into the VulkanBackend delegate. ghstack-source-id: 406366873 @exported-using-ghexport Differential Revision: [D112257609](https://our.internmc.facebook.com/intern/diff/D112257609/)
…> bool) Pull Request resolved: #21219 Problem: The WebGPU delegate has no elementwise comparison ops (`aten.eq/lt/le/gt/ge.Tensor`). They delegate through `VulkanPartitioner` (all 5 tagged, verified) and output BOOL tensors, but the backend had no handler and no bool-output path. Solution: Port all 5 comparisons via one shared handler + WGSL kernel (op-code switch), mirroring Vulkan `impl/BinaryOp.cpp` + `glsl/binary_op_buffer.yaml`. Output is a 1-byte bool tensor (`vk_datatype_size(BOOL)=1`, `is_int=true`, `is_int8=false`); the kernel packs 4 bool bytes per `u32` word (reusing the int8 buffer-binding idiom), and `copy_outputs` raw-copies it (dst==map, no widen). Per element: `out = (a OP b) ? 1 : 0` with OP in {==, <, <=, >, >=}. Implementation: `compare/Compare.cpp` registers `aten.eq/lt/le/gt/ge.Tensor` -> `compare_impl(graph, args, op)` (op 0=eq..4=ge). `compare.wgsl`: one thread per output word (4 bool bytes), 2D-folded dispatch, op switch. `CompareParams` (16 bytes: num_elements, op) matches the WGSL Params. Guards: fp32 inputs, 1-byte bool output, same-shape (in/out numel equal), `numel%4==0` (bool packs 4/word AND gates the readback map) — all fail-loud. Resize hook recomputes numel/dispatch + re-applies the %4 guard + cross-checks both operands. Constraints / divergences from the Vulkan reference: (1) `numel%4==0` (bool-output packing; fail-loud otherwise, mirroring the q8ta N%4 output-pack constraint). (2) same-shape only (flat kernel; broadcast is export-smoke, like `add`/`minimum`). (3) `eq` is torch-EXACT `a==b` — a DELIBERATE deviation from Vulkan's float `abs(X-Y)<1e-5`: the delegate serves torch-exact model semantics and the op-test golden is torch eager; `lt/le/gt/ge` mirror Vulkan's exact comparators. ghstack-source-id: 406366878 @exported-using-ghexport Differential Revision: [D112257588](https://our.internmc.facebook.com/intern/diff/D112257588/)
…rness Pull Request resolved: #21220 Problem: The new comparison ops (`aten.eq/lt/le/gt/ge.Tensor`) output BOOL, which the op-test golden harness did not handle (only fp32/int8/int64). And the goldens must actually discriminate the comparators. Solution: Extend the golden harness with a `bool` branch (mirrors the int8/int64 branches): `generate_op_tests` writes the bool golden as 0/1 bytes (`raw.dtype==torch.bool -> _write_int8(raw.to(int8))`, dtype "bool") and `op_test_driver` reads the bool output via `const_data_ptr<bool>()` and byte-compares. The change is additive (bool is a distinct dtype; the branch precedes int8) — existing fp32/int8/int64 paths are untouched (regression 27/27). `CompareModule(op)` covers all 5 ops; the two inputs draw from DIFFERENT discrete-range seeds (`compare_gen_a`/`compare_gen_b`) so `a!=b` with frequent collisions — giving `eq/le/ge` genuine ties and `lt/gt` a real true/false mix, so an op-switch swap diverges from the golden. Implementation: `cases.py` registers `eq`/`lt`/`le`/`gt`/`ge` via a shared `_compare_suite` (2d/3d/sq shapes, all numel%4==0); `test_compare.py` holds `CompareModule` + the seeded gens + a delegation smoke test. ghstack-source-id: 406388747 @exported-using-ghexport Differential Revision: [D112257586](https://our.internmc.facebook.com/intern/diff/D112257586/)
Pull Request resolved: #21221 Adds the elementwise bool `aten.logical_and.default` to the WebGPU delegate. Vulkan maps this op to its `bitwise_and` handler (`BinaryOp.cpp:161`), valid for canonical 0/1 bool bytes; this port mirrors that. Both operands and the output are 1-byte bool tensors packed 4/word into `array<u32>`, so a word-wise AND is exactly a per-byte AND. Key changes: - `runtime/ops/logical_and/logical_and.wgsl` — one thread per packed word, `t_out[w] = t_a[w] & t_b[w]` (2D-folded dispatch). - `runtime/ops/logical_and/LogicalAnd.cpp` — handler binds out (rw) + a/b (ro) + params (uniform); fail-loud guards on bool dtype, null buffer, and same-shape; resize hook re-applies `num_words` + dispatch. - `CMakeLists.txt` — register the op source. Same-shape only (`numel % 4 == 0` for the packed bool bindings), matching the backend's other bool ops. Co-authored-with: Claude Code. ghstack-source-id: 406388750 @exported-using-ghexport Differential Revision: [D112257653](https://our.internmc.facebook.com/intern/diff/D112257653/)
Pull Request resolved: #21222 Adds the `aten.logical_and.default` op-test suite to the manifest-driven WebGPU op-test framework, stacked above the op diff. Key changes: - `test/ops/test_logical_and.py` — `LogicalAndModule` derives its two bool operands on-GPU from float inputs (`a > 0`, `b > 0` via the delegated `gt.Tensor` against a baked zero buffer), so the only runtime inputs are the two float tensors (the op-test framework is float-input-only). Distinct `a`/`b` seeds make the two masks differ (each ~50% True, independent -> AND ~25% True), a real mix that an OR mutant would fail. `LogicalAndTest` is the export-delegation smoke test. - `test/op_tests/cases.py` — registers `logical_and` (shapes 2d/3d/sq, all `numel % 4 == 0`), byte-exact bool golden. Co-authored-with: Claude Code. ghstack-source-id: 406388749 @exported-using-ghexport Differential Revision: [D112257583](https://our.internmc.facebook.com/intern/diff/D112257583/)
Pull Request resolved: #21223 Adds the two bool bitwise ops the Vulkan partitioner tags (`op_registry.py` marks `bitwise_and.Tensor`, `bitwise_not.default`, `logical_and.default` all `BOOL_T`). `bitwise_and` on bool is identical to `logical_and`, so it shares the one AND handler — mirroring Vulkan, which registers both `aten.bitwise_and.Tensor` and `aten.logical_and.default` to its `bitwise_and` handler (`BinaryOp.cpp:160-161`). No new kernel needed. `bitwise_not` is the bool NOT: Vulkan uses `1 - X` on uint8 (`unary_op.yaml`); for canonical 0/1 that equals a per-byte low-bit flip, so the packed-word kernel is `t_out[w] = t_a[w] ^ 0x01010101u` (one thread per word). Key changes: - `runtime/ops/logical_and/LogicalAnd.cpp` — register `aten.bitwise_and.Tensor` on the existing shared bool-AND handler. - `runtime/ops/bitwise_not/{BitwiseNot.cpp,bitwise_not.wgsl}` — new unary; out (rw) + a (ro) + params (uniform); fail-loud bool-dtype/null/same-shape guards; resize hook re-applies `num_words`. - `CMakeLists.txt` — register `BitwiseNot.cpp`. Bool only (`numel % 4 == 0` for the packed bindings), matching the backend's other bool ops. Co-authored-with: Claude Code. ghstack-source-id: 406388751 @exported-using-ghexport Differential Revision: [D112257647](https://our.internmc.facebook.com/intern/diff/D112257647/)
Pull Request resolved: #21224 Adds the `aten.bitwise_and.Tensor` / `aten.bitwise_not.default` (bool) op-test suites, stacked above the op diff. Key changes: - `test/ops/test_bitwise.py` — `BitwiseAndModule`/`BitwiseNotModule` derive their bool operands on-GPU from float inputs (`a > 0` via the delegated `gt.Tensor` against a baked zero buffer), so the only runtime inputs are float tensors (the op-test framework is float-input-only). `bitwise_and` uses distinct `a`/`b` seeds (AND ~25% True); `bitwise_not` inverts one ~50% mask. - `test/op_tests/cases.py` — registers `bitwise_and`/`bitwise_not` (shapes 2d/3d/sq, all `numel % 4 == 0`), byte-exact bool goldens. Co-authored-with: Claude Code. ghstack-source-id: 406388753 @exported-using-ghexport Differential Revision: [D112257674](https://our.internmc.facebook.com/intern/diff/D112257674/)
… shared-memory Pull Request resolved: #21225 Problem: The reduction ops (amax/amin/argmax/sum/mean), softmax/log_softmax, and native_group_norm's reduce pass used naive one-thread-per-row serial loops — far below Vulkan's optimization level, where backends/vulkan/runtime/graph/ops/glsl/reduce.glsl uses a cooperative shared-memory reduction. Solution: Rewrite each to Vulkan's cooperative design — one workgroup per reduction row/group, each thread reduces a strided slice into a workgroup shared array, thread 0 aggregates the partials. Dispatch changed to one workgroup per row. argmax preserves torch's first-index tie-break; softmax keeps numerical stability + the log variant. ghstack-source-id: 406388754 @exported-using-ghexport Differential Revision: [D112257649](https://our.internmc.facebook.com/intern/diff/D112257649/)
…r, _to_copy) + enable unary activation family Pull Request resolved: #21226 Adds the creation + cast/copy ops and enables the unary activation family in the op allowlist. Key changes: - `runtime/ops/full/{Full.cpp, full.wgsl}` — `full`/`full_like`/`zeros`/`zeros_like`/`ones`/`ones_like`/`scalar_tensor` (shared fp32 fill; constant-folded before the delegate in practice, so effectively register-only) + a fail-loud non-fp32 output guard. - `runtime/ops/to_copy/{ToCopy.cpp, to_copy.wgsl}` — `_to_copy` / `_to_dim_order_copy`: same-dtype raw copy, and int<->float CONVERT via `f32(bitcast<i32>(x))` / `bitcast<f32>(i32(x))`. The dtype-promotion pass inserts an int->float `_to_copy` before binary ops, so a byte-reinterpret would ship denormal/inf garbage; this converts. Mirrors Vulkan `ToCopy.cpp` (view_convert vs blit branch). - `test/tester.py` — enable the unary activation family. ghstack-source-id: 406388756 @exported-using-ghexport Differential Revision: [D112257603](https://our.internmc.facebook.com/intern/diff/D112257603/)
…shape/movement, pooling, conv/sampling) Pull Request resolved: #21227 Problem: These ops already had kernels in the granular tree but were absent from WEBGPU_SUPPORTED_OPS, so the op-test framework neither delegated nor golden-tested them. Solution: Add to the allowlist: minimum, div.Tensor_mode (floor_divide), logical_and, bitwise_and, bitwise_not, flip, repeat, index_select, avg_pool2d, pixel_shuffle, convolution (depthwise conv1d), conv_with_clamp, grid_sampler_2d, grid_priors. ghstack-source-id: 406388757 @exported-using-ghexport Differential Revision: [D112257605](https://our.internmc.facebook.com/intern/diff/D112257605/)
…+ qcs4w/q8ta_q8csw linear) Pull Request resolved: #21228 Problem: The int8 quantized ops (q8ta elementwise/conv/linear + qcs4w and q8ta_q8csw linear) already had kernels in the granular tree but were absent from WEBGPU_SUPPORTED_OPS, so they were neither delegated nor golden-tested. Solution: Add to the allowlist: et_vk.linear_qcs4w, et_vk.linear_q8ta_q8csw, et_vk.q8ta_add, et_vk.q8ta_relu, et_vk.q8ta_pixel_shuffle, et_vk.q8ta_linear (+ q8ta_linear_gemv), et_vk.q8ta_conv2d, et_vk.q8ta_conv2d_dw, et_vk.q8ta_conv2d_pw. ghstack-source-id: 406388761 @exported-using-ghexport Differential Revision: [D112257636](https://our.internmc.facebook.com/intern/diff/D112257636/)
…8da4w) Pull Request resolved: #21229 Problem: WebGPU lacked the dynamic-8bit-activation x 4-bit-group-weight linear (the 8da4w path real LLMs use for dynamic quant). Vulkan registers `et_vk.linear_dq8ca_q4gsw` + `torchao.choose_qparams_affine`; WebGPU delegated neither, so any `Int8DynamicActivationIntxWeightConfig` model fell back to CPU. Solution: Author the reachable pair. `torchao.choose_qparams_affine` computes per-row asymmetric int8 activation scale/zp; `et_vk.linear_dq8ca_q4gsw` folds that dynamic activation quant into the existing q4gsw 4-bit-group GEMM. Both mirror the Vulkan reference (`ChooseQParams.cpp`, `QuantizedLinear.cpp:760`); the activation-quant + weight-dequant math was CPU-de-risked exact against torchao before authoring. Impl: `choose_qparams_affine.wgsl` does a cooperative per-row min/max reduction (one workgroup per block of 4 rows so the 4 int8 zps pack into one u32 with no write race) then `calculate_scale_and_zero_point` mirroring Vulkan. `linear_dq8ca_q4gsw.wgsl` is the register-tiled q4gsw GEMM with `out[m,n] = s[m] * sum_k (xq[m,k]-z[m]) * dequant(w)`, reading the packed-int8 zp. int8 zp (`elem_size` 1) is bound word-aligned; `num_rows` must be <=4 or a multiple of 4 (int8 buffers alloc `max(nbytes,4)`) — arbitrary-M prefill is a documented follow-up. ghstack-source-id: 406388760 @exported-using-ghexport Differential Revision: [D112257680](https://our.internmc.facebook.com/intern/diff/D112257680/)
Pull Request resolved: #21230 Add `aten.bitwise_or.Tensor` + `aten.logical_or.default` to the WebGPU backend — the OR counterpart to the existing `bitwise_and`/`logical_and` family, closing the last bool-binary parity gap with Vulkan. Both overloads share one handler (`logical_or_op`); on canonical 0/1 bool bytes the word-wise `|` over the packed `u32` buffer equals a per-byte OR. Kernel-only: the Vulkan partitioner already tags both ops (`op_registry.py` `register_bool_binary_ops`), so no partitioner change. Key changes: - `runtime/ops/logical_or/` — `logical_or.wgsl` (`t_out = t_a | t_b`, bool packed 4/word) + `LogicalOr.cpp` (one handler; bool-only + `numel % 4` guards + dynamic-shape resize hook; registers both overloads). Mirrors `logical_and`. - `test/ops/test_logical_or.py` + `op_tests/cases.py` — delegation smoke + op-test golden for both ops (two ~50% masks -> OR ~75% True, which distinguishes OR from an AND mutant). - `CMakeLists.txt` — wire `logical_or/LogicalOr.cpp`. Co-authored-with: Claude Code. ghstack-source-id: 406388764 @exported-using-ghexport Differential Revision: [D112483463](https://our.internmc.facebook.com/intern/diff/D112483463/)
… (0x30) Pull Request resolved: #21231 Rank-5+ tensors threw `0x30` (`DelegateInvalidCompatibility`) at delegate init: the shared `TensorMeta` UBO capped rank at 4 (`kTensorMetaMaxNdim`), so `fill_tensor_meta`/`fill_tensor_meta_broadcast` hard-throw during `build()` and the WGSL mirror stored `sizes`/`strides` as a single `vec4<u32>`. This clears `test_permute_different_shapes[webgpu]`, `test_transpose_different_shapes[webgpu]` (transpose lowers to `permute_copy`), and the rank>4 model failures (`conformer`, `maxvit_t`). Raise the global cap to 8 and widen the uniform to two `vec4<u32>` blocks, keeping the C++ and WGSL layouts byte-identical: - C++ `sizes[8]`/`strides[8]`: `sizeof(TensorMeta)` 48->80, `strides` offset 32->48 (`ndim`/`numel`/`sizes` offsets unchanged). - WGSL `sizes: array<vec4<u32>, 2>` / `strides: array<vec4<u32>, 2>`: in the uniform address space the `vec4<u32>` element stride is 16, so two blocks are 32 contiguous bytes matching `uint32_t[8]`; component k sits at byte `16 + 4*k` on both sides, so the raw memcpy stays correct. Runtime reads `m.strides[d]` become `m.strides[d >> 2u][d & 3u]`. - 8 (not 5): a WGSL uniform array cannot pack 5 `u32` tightly, so rank-5 would allocate the same two `vec4`s; 8 is the natural 2-`vec4` boundary and covers all realistic ranks. Key changes: - `runtime/ops/TensorMeta.h` - cap 4->8; `static_assert(sizeof == 80)` and `offsetof(strides) == 48`; throw text. - 15 shaders widened + reindexed (regenerated their 16 `_wgsl.h`): `permute`, `flip`, `cat`, `binary_op` (`div`/`sub`), `binary_mul`, `binary_pow`, `binary_minimum`, `binary_floor_divide`, `where`, `select`, `slice`, `expand_copy`, `gather`, `index_select`, `repeat`. - `permute`/`flip` also widen their per-dim `Params` array (`perm`/`flip` `vec4<u32>` -> `array<vec4<u32>, 2>`; `sizeof(PermuteParams)`/`sizeof(FlipParams)` 16->32). - Rank-guard throw messages "exceeds 4" -> "exceeds 8" (the guards themselves auto-follow the constant): `div`/`mul`/`sub`/`where`/`binary`/`index_select`/`repeat`. - Applied identically to both the `xplat/` and `fbcode/` mirrors (byte-identical). Co-authored-with: Claude Code. ghstack-source-id: 406388763 @exported-using-ghexport Differential Revision: [D113319595](https://our.internmc.facebook.com/intern/diff/D113319595/)
…match) Pull Request resolved: #21232 Under dynamic shapes `cat` produced a wrong (scrambled + tail-garbage) output. `cat_impl` baked every shape-dependent quantity - each input `in_meta` (strides/numel), the shared `out_meta` (strides), the per-input `off_k`, and each dispatch's `workgroup_count` - from the MAX (upper-bound) build shape and registered NO resize hook, then released the UBOs. Under a smaller live shape the kernel then decoded coords with max strides, scattered with max out-strides, and looped over the max numel. This is the `[dynamic]` output-mismatch behind `test_inception_v3`, `test_squeezenet1_1`, `test_densenet161` (all channel-cat on spatial-dynamic feature maps; the concat dim is static so `off_k` was already correct - the defect is the stale spatial strides/numel). Fix (mirrors the WebGPUGraph SwiGLU/QKV resize templates and the shipped `mul` hook): - Keep the per-input `in_meta`/`params` UBOs and the shared `out_meta` UBO alive via `own_uniform_buffer` (previously released after build); collect their handles plus each `add_dispatch` index. - Register an idempotent `add_tensor_resize_hook` on every input id: from `cur_dims` recompute live out dims (`set_cur_dims(out_id, ...)` to cascade to consumers + fix the delegate-output shape), rebuild `out_meta` + each input's `in_meta`/`params` and `wgpuQueueWriteBuffer` them, and rewrite each dispatch's `workgroup_count_x` via `compute_1d_workgroup_count`. On a static graph `cur_dims == dims`, so the hook rewrites identical values (no behavior change). Applied identically to both the `xplat/` and `fbcode/` mirrors (byte-identical). Co-authored-with: Claude Code. ghstack-source-id: 406388769 @exported-using-ghexport Differential Revision: [D113319596](https://our.internmc.facebook.com/intern/diff/D113319596/)
…dels Pull Request resolved: #21233 `add.Tensor` was elementwise-only: `binary_add.wgsl` computed `output[idx] = input1[idx] + alpha * input2[idx]` over a single flat index up to the output numel, so on a broadcast it read the smaller operand out of bounds (WebGPU robustness clamps/zeros -> silently wrong, not `0x30`). This is the failure mode behind the models the WebGPU flow skips (`resnet50`, `vit_b_16`, `swin_v2_t`, `convnext_small`, `mobilenet_v3_small`, `shufflenet_v2_x1_0`) and the `bcast_first`/`bcast_second` op cases. Give `add.Tensor` NumPy broadcasting by mirroring the shipped `mul`: - `binary_add.wgsl` - copy `binary_mul`'s kernel (rank-8 `TensorMeta` layout from the rank-cap fix below it in the stack): keep the identical-shape elementwise fast path (common case stays bit-for-bit `input1[idx] + alpha * input2[idx]`), else relinearize out idx -> per-input coords (clamp size-1 dims) and add. `alpha` is a second pipeline-override constant (read once at build, never rewritten on resize), so no extra UBO. - `add/BinaryOp.cpp` - port `mul_impl`: 3 `TensorMeta` UBOs via `fill_tensor_meta_broadcast`, rank + fp32 guards, 6-entry bind group, `constantCount = 2` ({wg_size, alpha}), 2D dispatch, and `mul`'s resize hook verbatim (rebuild the 3 metas from `cur_dims`, `set_cur_dims`, rewrite UBOs + dispatch); `own_uniform_buffer` the 3 metas. Drops the old flat `AddParams` path. - `flows/webgpu.py` - un-skip the 6 broadcast-add models + the `bcast_first`/`bcast_second` op cases (kept the `float16`/`float64` dtype skips and `hardswish`/`lstm_batch_sizes`/`upsample_nearest2d`). This sits above the rank-cap fix, so the shader uses the rank-8 `array<vec4<u32>, 2>` `TensorMeta` layout. Applied identically to both `xplat/` and `fbcode/` mirrors (byte-identical). Co-authored-with: Claude Code. ghstack-source-id: 406388773 @exported-using-ghexport Differential Revision: [D113319599](https://our.internmc.facebook.com/intern/diff/D113319599/)
This PR was created by the merge bot to help merge the original PR into the main branch.
ghstack PR number: #21153 by @JCNTH
^ Please use this as the source of truth for the PR details, comments, and reviews
ghstack PR base: https://github.com/pytorch/executorch/tree/gh/JCNTH/113/base
ghstack PR head: https://github.com/pytorch/executorch/tree/gh/JCNTH/113/head
Merge bot PR base: https://github.com/pytorch/executorch/tree/main
Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/JCNTH/113/orig
@diff-train-skip-merge