Skip to content

Commit bd00ba5

Browse files
authored
[ExecuTorch][WebGPU] Add constant_pad_nd op (#20857)
Stack from [ghstack](https://github.com/ezyang/ghstack/tree/0.15.0) (oldest at bottom): * #20868 * #20996 * #20995 * #20994 * #20993 * #20992 * #20991 * #20990 * #20989 * #20988 * #20987 * #20986 * #20876 * #20875 * #20874 * #20873 * #20872 * #20871 * #20866 * #20865 * #20864 * #20863 * #20862 * #20861 * #20860 * #20859 * #20858 * __->__ #20857 * #20856 * #20855 * #20854 * #20852 * #20851 * #20850 * #20849 * #20848 * #20846 * #20845 * #20844 * #20843 * #20842 **Adds `aten.constant_pad_nd.default` to the WebGPU backend, unblocking the DaViT window-padding path in vision models.** **Problem** — the backend had no `constant_pad_nd` handler, so any graph that pads a tensor (e.g. DaViT's window partitioning) could not fully delegate to WebGPU and threw at runtime. **Solution** — a single gather/fill compute kernel: Before — no handler; `aten.constant_pad_nd.default` unsupported at runtime. After — one thread per output element gathers the source element when its coordinates land inside the input, otherwise writes the constant fill `value`. **Implementation**: - The handler right-aligns the (rank 1..4) dims into fixed `vec4<u32>` params (`out_dims`, `in_dims`, `left`); leading slots get extent 1 / pad 0, so the WGSL is rank-agnostic and always iterates 4 dims. - The `pad` `IntList` is reversed-dim (innermost-first `(left, right)` pairs); the handler expands it to per-dim `left`/`right`, then validates `out.dims[d] == in.dims[d] + left[d] + right[d]` before any buffer allocation (loud-fail, no leak-on-throw). - The kernel decodes each output element's 4D coords (last dim fastest), subtracts each dim's `left` pad as an unsigned wrapping subtract (a negative coord wraps to a huge value and is rejected by the `< in_dims` bound check); if all four coords are in-bounds it copies `inp[flat_in]`, else it writes `value` — a pure copy/fill, so bit-exact. - The fill `value` is read via `utils::scalar_or` (a `Scalar` may serialize as `Int` or `Double`), defaulting to `0`. - Adaptive 1D->2D dispatch via `utils::compute_dispatch_grid` (workgroup size clamped to the device max, up to 256, plus a 2D spill past the 65535 workgroup-count ceiling; the `stride_x` override lets the shader decode `i = gid.y*stride_x + gid.x`). - Mirrors Vulkan `backends/vulkan/runtime/graph/ops/impl/Pad.cpp` (same reversed-dim `(before, after)` pad convention and `constant_pad_nd` resize logic). **Constraints** — fp32 only (`nbytes == numel*4` guard); rank 1..4; `pad` must be even-length and no longer than the rank; the output element count must fit `u32` (`<= 2^32`). Co-authored-with: Claude Code. @exported-using-ghexport Differential Revision: [D110836672](https://our.internmc.facebook.com/intern/diff/D110836672/) Differential Revision: [D110836672](https://our.internmc.facebook.com/intern/diff/D110836672)
1 parent ccb7cd7 commit bd00ba5

3 files changed

Lines changed: 306 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <executorch/backends/webgpu/runtime/WebGPUGraph.h>
10+
#include <executorch/backends/webgpu/runtime/WebGPUUtils.h>
11+
#include <executorch/backends/webgpu/runtime/ops/OperatorRegistry.h>
12+
#include <executorch/backends/webgpu/runtime/ops/constant_pad_nd/constant_pad_nd_wgsl.h>
13+
14+
#include <webgpu/webgpu.h>
15+
16+
#include <array>
17+
#include <cstdint>
18+
#include <stdexcept>
19+
20+
namespace executorch::backends::webgpu {
21+
22+
namespace {
23+
24+
struct PadParams {
25+
uint32_t out_dims[4];
26+
uint32_t in_dims[4];
27+
uint32_t left[4];
28+
uint32_t out_numel;
29+
float value;
30+
uint32_t _p0;
31+
uint32_t _p1;
32+
};
33+
static_assert(sizeof(PadParams) == 64, "PadParams must be 64 bytes");
34+
35+
// `pad` is reversed-dim (last-dim-first pairs); output right-aligned into 4D.
36+
void constant_pad_nd_impl(WebGPUGraph& graph, const std::vector<int>& args) {
37+
if (args.size() < 3) {
38+
throw std::runtime_error("WebGPU constant_pad_nd: expected >=3 args");
39+
}
40+
const int in_id = args.at(0);
41+
const int pad_id = args.at(1);
42+
const int out_id = args.at(args.size() - 1);
43+
44+
WGPUDevice device = graph.device();
45+
46+
const auto& in = graph.get_tensor(in_id);
47+
const auto& out = graph.get_tensor(out_id);
48+
49+
const size_t nd = in.dims.size();
50+
if (nd == 0 || nd > 4) {
51+
throw std::runtime_error("WebGPU constant_pad_nd: rank must be 1..4");
52+
}
53+
if (out.dims.size() != nd) {
54+
throw std::runtime_error("WebGPU constant_pad_nd: in/out rank mismatch");
55+
}
56+
57+
if (graph.get_value_type(pad_id) != WebGPUGraph::ValueType::IntList) {
58+
throw std::runtime_error("WebGPU constant_pad_nd: pad is not an IntList");
59+
}
60+
const std::vector<int64_t>& pad = graph.get_int_list(pad_id);
61+
if (pad.size() % 2 != 0) {
62+
throw std::runtime_error("WebGPU constant_pad_nd: pad must be even-length");
63+
}
64+
const size_t npad = pad.size() / 2;
65+
if (npad > nd) {
66+
throw std::runtime_error("WebGPU constant_pad_nd: pad longer than rank");
67+
}
68+
69+
// value scalar (default 0). Vulkan serializes a Scalar as Int or Double.
70+
float value = 0.0f;
71+
if (args.size() >= 4) {
72+
value = utils::scalar_or(graph, args.at(2), 0.0f);
73+
}
74+
75+
// Per-dim left/right pad (pad list is reversed-dim, from the LAST dim).
76+
std::array<int64_t, 4> left = {0, 0, 0, 0};
77+
std::array<int64_t, 4> right = {0, 0, 0, 0};
78+
for (size_t k = 0; k < npad; k++) {
79+
const size_t d = nd - 1 - k; // k-th pad entry -> dim (nd-1-k)
80+
left[d] = pad[2 * k];
81+
right[d] = pad[2 * k + 1];
82+
}
83+
84+
// Validate output dims == in + left + right per dim (loud-fail on a wrong
85+
// pad-list interpretation), before any buffer alloc -> no leak-on-throw.
86+
for (size_t d = 0; d < nd; d++) {
87+
// The kernel's pad params are u32; a negative pad (cropping) would wrap
88+
// into a huge offset, so reject it loudly rather than gather out of bounds.
89+
if (left[d] < 0 || right[d] < 0) {
90+
throw std::runtime_error(
91+
"WebGPU constant_pad_nd: negative pad (cropping) not supported");
92+
}
93+
const int64_t expect = in.dims[d] + left[d] + right[d];
94+
if (expect < 0 || static_cast<int64_t>(out.dims[d]) != expect) {
95+
throw std::runtime_error("WebGPU constant_pad_nd: output shape mismatch");
96+
}
97+
}
98+
99+
const uint64_t out_numel =
100+
utils::check_fp32(out, "constant_pad_nd", "output");
101+
utils::check_fp32(in, "constant_pad_nd", "input");
102+
103+
// Adaptive 1D->2D dispatch: wg=clamp(device,256) + 2D-spill past the 65535
104+
// ceiling. stride_x lets the shader decode idx = gid.y*stride_x + gid.x.
105+
utils::DispatchGrid grid = utils::compute_dispatch_grid(
106+
device,
107+
utils::checked_u32(out_numel, "constant_pad_nd"),
108+
kConstantPadNdWorkgroupSizeX,
109+
"constant_pad_nd");
110+
111+
// Right-align dims into [4]: leading (4-nd) entries get extent 1, pad 0.
112+
PadParams params = {};
113+
for (int s = 0; s < 4; s++) {
114+
params.out_dims[s] = 1;
115+
params.in_dims[s] = 1;
116+
params.left[s] = 0;
117+
}
118+
const size_t off = 4 - nd;
119+
for (size_t d = 0; d < nd; d++) {
120+
params.out_dims[off + d] = static_cast<uint32_t>(out.dims[d]);
121+
params.in_dims[off + d] = static_cast<uint32_t>(in.dims[d]);
122+
params.left[off + d] = static_cast<uint32_t>(left[d]);
123+
}
124+
params.out_numel = static_cast<uint32_t>(out_numel);
125+
params.value = value;
126+
127+
WGPUBuffer uniform_buffer =
128+
utils::make_uniform(device, &params, sizeof(PadParams));
129+
graph.add_uniform_buffer_bytes(sizeof(PadParams));
130+
131+
auto constants = utils::make_grid_constants(grid);
132+
133+
utils::ComputePipelineBundle bundle = utils::make_compute_pipeline(
134+
device,
135+
kConstantPadNdWGSL,
136+
{
137+
{0, WGPUBufferBindingType_Storage, out.buffer, out.nbytes},
138+
{1, WGPUBufferBindingType_ReadOnlyStorage, in.buffer, in.nbytes},
139+
{2, WGPUBufferBindingType_Uniform, uniform_buffer, sizeof(PadParams)},
140+
},
141+
constants.data(),
142+
constants.size());
143+
144+
graph.add_dispatch_2d(
145+
bundle.pipeline, bundle.bind_group, grid.count_x, grid.count_y);
146+
147+
wgpuBufferRelease(uniform_buffer);
148+
}
149+
150+
} // namespace
151+
152+
WEBGPU_REGISTER_OPERATORS {
153+
WEBGPU_REGISTER_OP(aten.constant_pad_nd.default, constant_pad_nd_impl);
154+
}
155+
156+
} // namespace executorch::backends::webgpu
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
@group(0) @binding(0) var<storage, read_write> out: array<f32>;
2+
@group(0) @binding(1) var<storage, read> inp: array<f32>;
3+
4+
// Up to 4D. The handler right-aligns dims into [4] (leading entries = 1, left/
5+
// right pad = 0 for unpadded/leading dims), so the shader is rank-agnostic and
6+
// always iterates 4 dims. in_dims[d] = input extent, left[d] = that dim's
7+
// left-pad, out_dims[d] = in_dims[d] + left[d] + right[d].
8+
struct Params {
9+
out_dims: vec4<u32>,
10+
in_dims: vec4<u32>,
11+
left: vec4<u32>,
12+
out_numel: u32,
13+
value: f32,
14+
_p0: u32,
15+
_p1: u32,
16+
}
17+
@group(0) @binding(2) var<uniform> params: Params;
18+
19+
override wg_size: u32 = 256;
20+
override stride_x: u32 = 4294967295u; // = count_x * wg_size; set by host for 2D-spill
21+
22+
// constant_pad_nd, gather form, NCHW row-major fp32. One thread per OUTPUT
23+
// element: decode its 4D coords, subtract each dim's left-pad to get the input
24+
// coord; if ALL input coords are in-bounds -> copy inp[flat_in], else write
25+
// `value`. Pure copy/fill -> bit-exact. (CPU-derisked == torch at 0.)
26+
@compute @workgroup_size(wg_size)
27+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
28+
let i = gid.y * stride_x + gid.x;
29+
if (i >= params.out_numel) {
30+
return;
31+
}
32+
33+
// decode out coords (last dim fastest)
34+
var rem = i;
35+
let o3 = rem % params.out_dims.w;
36+
rem = rem / params.out_dims.w;
37+
let o2 = rem % params.out_dims.z;
38+
rem = rem / params.out_dims.z;
39+
let o1 = rem % params.out_dims.y;
40+
rem = rem / params.out_dims.y;
41+
let o0 = rem % params.out_dims.x;
42+
43+
// subtract left pad -> input coord (wrapping subtract; check via < in_dim on
44+
// the unsigned result catches negatives because they wrap to huge values)
45+
let c0 = o0 - params.left.x;
46+
let c1 = o1 - params.left.y;
47+
let c2 = o2 - params.left.z;
48+
let c3 = o3 - params.left.w;
49+
50+
let in0 = o0 >= params.left.x && c0 < params.in_dims.x;
51+
let in1 = o1 >= params.left.y && c1 < params.in_dims.y;
52+
let in2 = o2 >= params.left.z && c2 < params.in_dims.z;
53+
let in3 = o3 >= params.left.w && c3 < params.in_dims.w;
54+
55+
if (in0 && in1 && in2 && in3) {
56+
let in_idx =
57+
((c0 * params.in_dims.y + c1) * params.in_dims.z + c2) * params.in_dims.w
58+
+ c3;
59+
out[i] = inp[in_idx];
60+
} else {
61+
out[i] = params.value;
62+
}
63+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#pragma once
10+
11+
#include <cstdint>
12+
13+
namespace executorch::backends::webgpu {
14+
15+
// @generated from constant_pad_nd.wgsl - DO NOT EDIT.
16+
// wgsl-sha256: 67496c3b851bbe69c64d26bcca415cd4c0de20d555b0ed5cccbe66499a757592
17+
inline constexpr const char* kConstantPadNdWGSL = R"(
18+
@group(0) @binding(0) var<storage, read_write> out: array<f32>;
19+
@group(0) @binding(1) var<storage, read> inp: array<f32>;
20+
21+
// Up to 4D. The handler right-aligns dims into [4] (leading entries = 1, left/
22+
// right pad = 0 for unpadded/leading dims), so the shader is rank-agnostic and
23+
// always iterates 4 dims. in_dims[d] = input extent, left[d] = that dim's
24+
// left-pad, out_dims[d] = in_dims[d] + left[d] + right[d].
25+
struct Params {
26+
out_dims: vec4<u32>,
27+
in_dims: vec4<u32>,
28+
left: vec4<u32>,
29+
out_numel: u32,
30+
value: f32,
31+
_p0: u32,
32+
_p1: u32,
33+
}
34+
@group(0) @binding(2) var<uniform> params: Params;
35+
36+
override wg_size: u32 = 256;
37+
override stride_x: u32 = 4294967295u; // = count_x * wg_size; set by host for 2D-spill
38+
39+
// constant_pad_nd, gather form, NCHW row-major fp32. One thread per OUTPUT
40+
// element: decode its 4D coords, subtract each dim's left-pad to get the input
41+
// coord; if ALL input coords are in-bounds -> copy inp[flat_in], else write
42+
// `value`. Pure copy/fill -> bit-exact. (CPU-derisked == torch at 0.)
43+
@compute @workgroup_size(wg_size)
44+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
45+
let i = gid.y * stride_x + gid.x;
46+
if (i >= params.out_numel) {
47+
return;
48+
}
49+
50+
// decode out coords (last dim fastest)
51+
var rem = i;
52+
let o3 = rem % params.out_dims.w;
53+
rem = rem / params.out_dims.w;
54+
let o2 = rem % params.out_dims.z;
55+
rem = rem / params.out_dims.z;
56+
let o1 = rem % params.out_dims.y;
57+
rem = rem / params.out_dims.y;
58+
let o0 = rem % params.out_dims.x;
59+
60+
// subtract left pad -> input coord (wrapping subtract; check via < in_dim on
61+
// the unsigned result catches negatives because they wrap to huge values)
62+
let c0 = o0 - params.left.x;
63+
let c1 = o1 - params.left.y;
64+
let c2 = o2 - params.left.z;
65+
let c3 = o3 - params.left.w;
66+
67+
let in0 = o0 >= params.left.x && c0 < params.in_dims.x;
68+
let in1 = o1 >= params.left.y && c1 < params.in_dims.y;
69+
let in2 = o2 >= params.left.z && c2 < params.in_dims.z;
70+
let in3 = o3 >= params.left.w && c3 < params.in_dims.w;
71+
72+
if (in0 && in1 && in2 && in3) {
73+
let in_idx =
74+
((c0 * params.in_dims.y + c1) * params.in_dims.z + c2) * params.in_dims.w
75+
+ c3;
76+
out[i] = inp[in_idx];
77+
} else {
78+
out[i] = params.value;
79+
}
80+
}
81+
)";
82+
83+
inline constexpr uint32_t kConstantPadNdWorkgroupSizeX = 256;
84+
inline constexpr uint32_t kConstantPadNdWorkgroupSizeY = 1;
85+
inline constexpr uint32_t kConstantPadNdWorkgroupSizeZ = 1;
86+
87+
} // namespace executorch::backends::webgpu

0 commit comments

Comments
 (0)