Skip to content

Commit 9040515

Browse files
committed
[ExecuTorch][WebGPU] Add select/boolean ops (where, scalar compares, logical_not) to the WebGPU backend
Pull Request resolved: #20925 Add `aten.where.self`, scalar comparisons (`aten.{eq,ne,le,ge,lt,gt}.Scalar`), and `aten.logical_not.default` handlers for the training tail. The scalar compares and `logical_not` are generated from ONE templated byte-packed-bool shader (`runtime/ops/boolean_op/`) rather than per-op hand-written kernels — mirroring the `binary_op` codegen and Vulkan's grouping of comparison ops as `${OPERATOR}` variants (`backends/vulkan/runtime/graph/ops/glsl/binary_op_buffer.yaml`). Key changes: - `runtime/ops/boolean_op/` — one `boolean_op.wgsl` template + `boolean_op.yaml` (variants `compare_{eq,ne,le,ge,lt,gt}` + `logical_not`) expand to the per-variant `*_wgsl.h`; `BooleanOp.cpp` holds one shared `dispatch_bool_op` helper (pack 4 bools per `u32` word, one thread per word) plus the seven `WEBGPU_REGISTER_OP`s. - `runtime/ops/where/` — ternary select, kept as its own kernel/handler (non-bool-family, as in Vulkan). - `op_registry.py` — register the net-new `aten.{ne,lt,le,ge,gt}.Scalar` + `aten.logical_not.default` `OpFeatures` (`eq.Scalar` already on master). - `CMakeLists.txt` `WEBGPU_SRCS` — wire `boolean_op/BooleanOp.cpp`. `aten.where.self` already had Vulkan `OpFeatures`; the scalar-compare + `logical_not` registrations are additive to the shared Vulkan registry. Co-authored-with: Claude Code. ghstack-source-id: 405026062 @exported-using-ghexport Differential Revision: [D111755122](https://our.internmc.facebook.com/intern/diff/D111755122/)
1 parent 2c56404 commit 9040515

15 files changed

Lines changed: 1129 additions & 0 deletions

backends/vulkan/op_registry.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,6 +1746,35 @@ def register_rms_norm():
17461746
)
17471747

17481748

1749+
@update_features(
1750+
[
1751+
exir_ops.edge.aten.ne.Scalar,
1752+
exir_ops.edge.aten.lt.Scalar,
1753+
exir_ops.edge.aten.le.Scalar,
1754+
exir_ops.edge.aten.ge.Scalar,
1755+
exir_ops.edge.aten.gt.Scalar,
1756+
]
1757+
)
1758+
def register_compare_scalar_ops():
1759+
return OpFeatures(
1760+
inputs_storage=utils.ANY_STORAGE,
1761+
inputs_dtypes=utils.FP_INT_T,
1762+
outputs_dtypes=utils.BOOL_T,
1763+
supports_resize=True,
1764+
supports_highdim=True,
1765+
)
1766+
1767+
1768+
@update_features(exir_ops.edge.aten.logical_not.default)
1769+
def register_logical_not():
1770+
return OpFeatures(
1771+
inputs_storage=utils.ANY_STORAGE,
1772+
inputs_dtypes=utils.BOOL_T,
1773+
supports_resize=True,
1774+
supports_highdim=True,
1775+
)
1776+
1777+
17491778
#######################
17501779
## Utility functions ##
17511780
#######################

backends/webgpu/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ set(WEBGPU_SRCS
5959
runtime/ops/reduce/Reduce.cpp
6060
runtime/ops/div/BinaryOp.cpp
6161
runtime/ops/sub/BinaryOp.cpp
62+
runtime/ops/where/Where.cpp
63+
runtime/ops/boolean_op/BooleanOp.cpp
6264
runtime/ops/linear/Linear.cpp
6365
)
6466

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
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/boolean_op/compare_eq_wgsl.h>
13+
#include <executorch/backends/webgpu/runtime/ops/boolean_op/compare_ge_wgsl.h>
14+
#include <executorch/backends/webgpu/runtime/ops/boolean_op/compare_gt_wgsl.h>
15+
#include <executorch/backends/webgpu/runtime/ops/boolean_op/compare_le_wgsl.h>
16+
#include <executorch/backends/webgpu/runtime/ops/boolean_op/compare_lt_wgsl.h>
17+
#include <executorch/backends/webgpu/runtime/ops/boolean_op/compare_ne_wgsl.h>
18+
#include <executorch/backends/webgpu/runtime/ops/boolean_op/logical_not_wgsl.h>
19+
20+
#include <webgpu/webgpu.h>
21+
22+
#include <cstdint>
23+
#include <stdexcept>
24+
#include <string>
25+
#include <vector>
26+
27+
namespace executorch::backends::webgpu {
28+
29+
namespace {
30+
31+
// Shared uniform for the byte-packed-bool op family. `scalar` is unused (0) for
32+
// the scalar-less (unary) logical_not variant.
33+
struct BoolOpParams {
34+
uint32_t num_elements;
35+
float scalar;
36+
uint32_t _pad1;
37+
uint32_t _pad2;
38+
};
39+
40+
float read_scalar(WebGPUGraph& graph, int id, const char* op_name) {
41+
if (graph.get_value_type(id) == WebGPUGraph::ValueType::Double) {
42+
return static_cast<float>(graph.get_double(id));
43+
}
44+
if (graph.get_value_type(id) == WebGPUGraph::ValueType::Int) {
45+
return static_cast<float>(graph.get_int(id));
46+
}
47+
throw std::runtime_error(std::string(op_name) + ": scalar is not int/double");
48+
}
49+
50+
// Dispatch a byte-packed-bool op (scalar compares + logical_not): read an input
51+
// buffer, write a u32 output packing 4 bool bytes per word, one thread per word
52+
// (no inter-thread write race). The caller validates dtypes/shapes and supplies
53+
// `numel` (logical element count = prod(dims)), the scalar (0 when unused), and
54+
// the per-variant shader from boolean_op.yaml.
55+
void dispatch_bool_op(
56+
WebGPUGraph& graph,
57+
int self_id,
58+
int out_id,
59+
uint32_t numel,
60+
float scalar,
61+
const char* wgsl,
62+
uint32_t wg_size_x,
63+
const char* op_name) {
64+
WGPUDevice device = graph.device();
65+
const auto& self_tensor = graph.get_tensor(self_id);
66+
const auto& out_tensor = graph.get_tensor(out_id);
67+
68+
// The output (and logical_not's packed-bool input) is a u32 array; round the
69+
// binding up to whole words even when the byte count isn't a multiple of 4.
70+
const size_t self_bind_size = (self_tensor.nbytes + 3) & ~size_t(3);
71+
const size_t out_bind_size = (out_tensor.nbytes + 3) & ~size_t(3);
72+
const uint32_t n_words = (numel + 3u) / 4u;
73+
74+
uint32_t wg_size = utils::clamp_workgroup_size(device, wg_size_x);
75+
uint32_t workgroup_count =
76+
utils::compute_1d_workgroup_count(device, n_words, wg_size, op_name);
77+
78+
WGPUConstantEntry wg_size_constant = {};
79+
wg_size_constant.key = {"wg_size", WGPU_STRLEN};
80+
wg_size_constant.value = static_cast<double>(wg_size);
81+
82+
BoolOpParams params = {numel, scalar, 0u, 0u};
83+
WGPUBuffer params_buf =
84+
utils::make_uniform(device, &params, sizeof(BoolOpParams));
85+
graph.add_uniform_buffer_bytes(sizeof(BoolOpParams));
86+
87+
WGPUShaderSourceWGSL wgsl_desc = {};
88+
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
89+
wgsl_desc.code = {wgsl, WGPU_STRLEN};
90+
WGPUShaderModuleDescriptor shader_desc = {};
91+
shader_desc.nextInChain = &wgsl_desc.chain;
92+
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
93+
94+
WGPUBindGroupLayoutEntry entries[3] = {};
95+
entries[0].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
96+
entries[1].buffer.type = WGPUBufferBindingType_Storage;
97+
entries[2].buffer.type = WGPUBufferBindingType_Uniform;
98+
for (uint32_t i = 0; i < 3; i++) {
99+
entries[i].binding = i;
100+
entries[i].visibility = WGPUShaderStage_Compute;
101+
}
102+
103+
WGPUBindGroupLayoutDescriptor bgl_desc = {};
104+
bgl_desc.entryCount = 3;
105+
bgl_desc.entries = entries;
106+
WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
107+
108+
WGPUPipelineLayoutDescriptor pl_desc = {};
109+
pl_desc.bindGroupLayoutCount = 1;
110+
pl_desc.bindGroupLayouts = &bgl;
111+
WGPUPipelineLayout pipeline_layout =
112+
wgpuDeviceCreatePipelineLayout(device, &pl_desc);
113+
114+
WGPUComputePipelineDescriptor pipeline_desc = {};
115+
pipeline_desc.layout = pipeline_layout;
116+
pipeline_desc.compute.module = shader;
117+
pipeline_desc.compute.entryPoint = {"main", WGPU_STRLEN};
118+
pipeline_desc.compute.constantCount = 1;
119+
pipeline_desc.compute.constants = &wg_size_constant;
120+
WGPUComputePipeline pipeline =
121+
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
122+
123+
WGPUBindGroupEntry bg_entries[3] = {};
124+
bg_entries[0].binding = 0;
125+
bg_entries[0].buffer = self_tensor.buffer;
126+
bg_entries[0].size = self_bind_size;
127+
bg_entries[1].binding = 1;
128+
bg_entries[1].buffer = out_tensor.buffer;
129+
bg_entries[1].size = out_bind_size;
130+
bg_entries[2].binding = 2;
131+
bg_entries[2].buffer = params_buf;
132+
bg_entries[2].size = sizeof(BoolOpParams);
133+
134+
WGPUBindGroupDescriptor bg_desc = {};
135+
bg_desc.layout = bgl;
136+
bg_desc.entryCount = 3;
137+
bg_desc.entries = bg_entries;
138+
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
139+
140+
const size_t dispatch_idx =
141+
graph.add_dispatch({pipeline, bind_group, workgroup_count});
142+
143+
WGPUBuffer p_buf = params_buf;
144+
auto resize = [self_id, out_id, scalar, wg_size, dispatch_idx, p_buf, op_name](
145+
WebGPUGraph& g) {
146+
const auto& d = g.cur_dims(self_id);
147+
uint32_t n = 1u;
148+
for (auto x : d) {
149+
n *= static_cast<uint32_t>(x);
150+
}
151+
g.set_cur_dims(out_id, d);
152+
BoolOpParams p = {n, scalar, 0u, 0u};
153+
wgpuQueueWriteBuffer(g.queue(), p_buf, 0, &p, sizeof(p));
154+
const uint32_t nw = (n + 3u) / 4u;
155+
g.dispatch_at(dispatch_idx).workgroup_count_x =
156+
utils::compute_1d_workgroup_count(g.device(), nw, wg_size, op_name);
157+
};
158+
graph.add_tensor_resize_hook(self_id, resize);
159+
160+
wgpuShaderModuleRelease(shader);
161+
wgpuBindGroupLayoutRelease(bgl);
162+
wgpuPipelineLayoutRelease(pipeline_layout);
163+
graph.own_uniform_buffer(params_buf);
164+
}
165+
166+
// cmp(self[i], scalar) -> byte-packed bool. args: [self, scalar, out].
167+
void compare_dispatch(
168+
WebGPUGraph& graph,
169+
const std::vector<int>& args,
170+
const char* wgsl,
171+
uint32_t wg_size_x,
172+
const char* op_name) {
173+
const int self_id = args.at(0);
174+
const int out_id = args.at(args.size() - 1);
175+
const float scalar = read_scalar(graph, args.at(1), op_name);
176+
177+
const auto& self_tensor = graph.get_tensor(self_id);
178+
const auto& out_tensor = graph.get_tensor(out_id);
179+
if (self_tensor.buffer == nullptr || out_tensor.buffer == nullptr) {
180+
throw std::runtime_error(std::string(op_name) + ": null buffer binding");
181+
}
182+
if (self_tensor.nbytes % sizeof(float) != 0) {
183+
throw std::runtime_error(std::string(op_name) + ": self is not fp32");
184+
}
185+
const uint32_t numel =
186+
static_cast<uint32_t>(self_tensor.nbytes / sizeof(float));
187+
if (out_tensor.nbytes != static_cast<size_t>(numel)) {
188+
throw std::runtime_error(
189+
std::string(op_name) + ": out is not a 1-byte (bool) tensor");
190+
}
191+
dispatch_bool_op(
192+
graph, self_id, out_id, numel, scalar, wgsl, wg_size_x, op_name);
193+
}
194+
195+
void eq_scalar_impl(WebGPUGraph& g, const std::vector<int>& a) {
196+
compare_dispatch(g, a, kCompareEqWGSL, kCompareEqWorkgroupSizeX, "eq.Scalar");
197+
}
198+
void ne_scalar_impl(WebGPUGraph& g, const std::vector<int>& a) {
199+
compare_dispatch(g, a, kCompareNeWGSL, kCompareNeWorkgroupSizeX, "ne.Scalar");
200+
}
201+
void le_scalar_impl(WebGPUGraph& g, const std::vector<int>& a) {
202+
compare_dispatch(g, a, kCompareLeWGSL, kCompareLeWorkgroupSizeX, "le.Scalar");
203+
}
204+
void ge_scalar_impl(WebGPUGraph& g, const std::vector<int>& a) {
205+
compare_dispatch(g, a, kCompareGeWGSL, kCompareGeWorkgroupSizeX, "ge.Scalar");
206+
}
207+
void lt_scalar_impl(WebGPUGraph& g, const std::vector<int>& a) {
208+
compare_dispatch(g, a, kCompareLtWGSL, kCompareLtWorkgroupSizeX, "lt.Scalar");
209+
}
210+
void gt_scalar_impl(WebGPUGraph& g, const std::vector<int>& a) {
211+
compare_dispatch(g, a, kCompareGtWGSL, kCompareGtWorkgroupSizeX, "gt.Scalar");
212+
}
213+
214+
// logical_not: byte-packed bool -> byte-packed bool. args: [self, out].
215+
void logical_not_impl(WebGPUGraph& graph, const std::vector<int>& args) {
216+
const int self_id = args.at(0);
217+
const int out_id = args.at(args.size() - 1);
218+
219+
const auto& self_tensor = graph.get_tensor(self_id);
220+
const auto& out_tensor = graph.get_tensor(out_id);
221+
if (self_tensor.buffer == nullptr || out_tensor.buffer == nullptr) {
222+
throw std::runtime_error("logical_not: null buffer binding");
223+
}
224+
if (out_tensor.nbytes != self_tensor.nbytes) {
225+
throw std::runtime_error("logical_not: self/out byte-size mismatch");
226+
}
227+
const uint32_t numel = static_cast<uint32_t>(self_tensor.nbytes);
228+
dispatch_bool_op(
229+
graph,
230+
self_id,
231+
out_id,
232+
numel,
233+
0.0f,
234+
kLogicalNotWGSL,
235+
kLogicalNotWorkgroupSizeX,
236+
"logical_not");
237+
}
238+
239+
} // namespace
240+
241+
WEBGPU_REGISTER_OPERATORS {
242+
WEBGPU_REGISTER_OP(aten.eq.Scalar, eq_scalar_impl);
243+
WEBGPU_REGISTER_OP(aten.ne.Scalar, ne_scalar_impl);
244+
WEBGPU_REGISTER_OP(aten.le.Scalar, le_scalar_impl);
245+
WEBGPU_REGISTER_OP(aten.ge.Scalar, ge_scalar_impl);
246+
WEBGPU_REGISTER_OP(aten.lt.Scalar, lt_scalar_impl);
247+
WEBGPU_REGISTER_OP(aten.gt.Scalar, gt_scalar_impl);
248+
WEBGPU_REGISTER_OP(aten.logical_not.default, logical_not_impl);
249+
}
250+
251+
} // namespace executorch::backends::webgpu
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@group(0) @binding(0) var<storage, read> input: array<${IN_TYPE}>;
2+
@group(0) @binding(1) var<storage, read_write> output: array<u32>;
3+
4+
struct Params {
5+
num_elements: u32,
6+
scalar: f32,
7+
_pad1: u32,
8+
_pad2: u32,
9+
}
10+
@group(0) @binding(2) var<uniform> params: Params;
11+
12+
override wg_size: u32 = 64u;
13+
14+
// Per-variant predicate substituted from boolean_op.yaml (compare / logical_not).
15+
fn elem_bool(i: u32) -> bool {
16+
return ${OP_EXPR};
17+
}
18+
19+
// One thread per output u32 word packs 4 bool bytes -> no inter-thread race.
20+
@compute @workgroup_size(wg_size, 1, 1)
21+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
22+
let word_idx = gid.x;
23+
let n_words = (params.num_elements + 3u) / 4u;
24+
if (word_idx >= n_words) {
25+
return;
26+
}
27+
var packed: u32 = 0u;
28+
for (var j: u32 = 0u; j < 4u; j = j + 1u) {
29+
let i = word_idx * 4u + j;
30+
if (i < params.num_elements) {
31+
if (elem_bool(i)) {
32+
packed = packed | (1u << (j * 8u));
33+
}
34+
}
35+
}
36+
output[word_idx] = packed;
37+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
boolean_op:
2+
parameter_names_with_default_values:
3+
IN_TYPE: f32
4+
OP_EXPR: input[i] == params.scalar
5+
shader_variants:
6+
- NAME: compare_eq
7+
OP_EXPR: input[i] == params.scalar
8+
- NAME: compare_ne
9+
OP_EXPR: input[i] != params.scalar
10+
- NAME: compare_le
11+
OP_EXPR: input[i] <= params.scalar
12+
- NAME: compare_ge
13+
OP_EXPR: input[i] >= params.scalar
14+
- NAME: compare_lt
15+
OP_EXPR: input[i] < params.scalar
16+
- NAME: compare_gt
17+
OP_EXPR: input[i] > params.scalar
18+
- NAME: logical_not
19+
IN_TYPE: u32
20+
OP_EXPR: ((input[i >> 2u] >> ((i & 3u) * 8u)) & 0xFFu) == 0u

0 commit comments

Comments
 (0)