Skip to content

Commit 7e3583c

Browse files
committed
[ExecuTorch][WebGPU] Add cat op + ValueList graph support (aten.cat.default)
Pull Request resolved: #20398 Adds `aten.cat.default` to the WebGPU delegate as an index-math scatter, and the `ValueList` graph value type it needs to read its tensor-list argument. Composition (one dispatch per input): - `runtime/WebGPUGraph.{h,cpp}` — adds `ValueType::ValueList` backed by `std::vector<std::vector<int>> value_lists_` + `get_value_list(int)`; `build()` deserializes `vkgraph::GraphTypes::ValueList` via `value_as_ValueList()->items()`; mirrors the existing scalar value plumbing. - `runtime/ops/cat/Cat.cpp` — reads the input-tensor list via `get_value_list`, reads the static `Int` dim, validates each input rank + non-concat dims against the output; builds one shared `out_meta` `TensorMeta` uniform + a shared bind-group/pipeline layout; per input builds a fresh pipeline + bind group with `in_meta` + `CatParams{concat_dim, off_k}`, dispatches over `compute_1d_workgroup_count(in.numel)`; releases per-input uniforms after each bind group, the shared `out_meta` after the loop. - `runtime/ops/cat/cat.wgsl` — delinearizes the input index over the input strides, shifts the concat-dim coordinate by the host-computed `off_k` (running sum of prior input sizes), relinearizes over the output strides to scatter `output[out] = input[in]`. - The N disjoint-slab writes share one output buffer across separate dispatches and rely on the existing per-pass `execute()` ordering. ghstack-source-id: 397534680 @exported-using-ghexport @diff-train-skip-merge Differential Revision: [D108793165](https://our.internmc.facebook.com/intern/diff/D108793165/)
1 parent 200f64a commit 7e3583c

6 files changed

Lines changed: 341 additions & 15 deletions

File tree

backends/webgpu/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ set(WEBGPU_SRCS
4949
runtime/ops/unsqueeze/Unsqueeze.cpp
5050
runtime/ops/slice/Slice.cpp
5151
runtime/ops/permute/Permute.cpp
52+
runtime/ops/cat/Cat.cpp
5253
)
5354

5455
add_library(webgpu_backend ${WEBGPU_SRCS})

backends/webgpu/runtime/WebGPUGraph.cpp

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,9 @@ void WebGPUGraph::build(
246246
tensor_mem_obj_ids_.resize(num_vals, -1);
247247
ints_.resize(num_vals, 0);
248248
int_lists_.resize(num_vals);
249+
value_lists_.resize(num_vals);
249250
doubles_.resize(num_vals, 0.0);
250251
bools_.resize(num_vals, false);
251-
value_lists_.resize(num_vals);
252252

253253
// Pre-scan the op chain: a constant may be DEFERRED (no eager GPU buffer; the
254254
// prepack node materializes it once) only if it is a prepack source AND never
@@ -384,6 +384,17 @@ void WebGPUGraph::build(
384384
}
385385
break;
386386
}
387+
case vkgraph::GraphTypes::ValueList: {
388+
value_types_[i] = ValueType::ValueList;
389+
const auto* items = val->value_as_ValueList()->items();
390+
if (items) {
391+
value_lists_[i].reserve(items->size());
392+
for (unsigned j = 0; j < items->size(); j++) {
393+
value_lists_[i].push_back(static_cast<int>(items->Get(j)));
394+
}
395+
}
396+
break;
397+
}
387398
case vkgraph::GraphTypes::Double: {
388399
value_types_[i] = ValueType::Double;
389400
doubles_[i] = val->value_as_Double()->double_val();
@@ -415,16 +426,6 @@ void WebGPUGraph::build(
415426
add_uniform_buffer_bytes(kSymIntUniformBytes);
416427
break;
417428
}
418-
case vkgraph::GraphTypes::ValueList: {
419-
value_types_[i] = ValueType::ValueList;
420-
const auto* items = val->value_as_ValueList()->items();
421-
if (items) {
422-
for (unsigned j = 0; j < items->size(); j++) {
423-
value_lists_[i].push_back(static_cast<int>(items->Get(j)));
424-
}
425-
}
426-
break;
427-
}
428429
default:
429430
value_types_[i] = ValueType::Null;
430431
break;

backends/webgpu/runtime/WebGPUGraph.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,13 @@ class WebGPUGraph {
136136
const std::vector<int64_t>& get_int_list(int id) const {
137137
return int_lists_[id];
138138
}
139-
bool get_bool(int id) const {
140-
return bools_[id];
141-
}
142139
// Member value ids of a serialized ValueList (op multi-output list).
143140
const std::vector<int>& get_value_list(int id) const {
144141
return value_lists_[id];
145142
}
143+
bool get_bool(int id) const {
144+
return bools_[id];
145+
}
146146

147147
// Live-scalar (SymInt) API; mirrors the Vulkan SymInt/ParamsBuffer UBO.
148148
// set_symint writes the buffer + marks dirty only if the value changed.
@@ -282,9 +282,9 @@ class WebGPUGraph {
282282
std::vector<WebGPUTensor> tensors_;
283283
std::vector<int64_t> ints_;
284284
std::vector<std::vector<int64_t>> int_lists_;
285+
std::vector<std::vector<int>> value_lists_;
285286
std::vector<double> doubles_;
286287
std::vector<bool> bools_;
287-
std::vector<std::vector<int>> value_lists_;
288288

289289
// SymInt (live scalar): id -> {live Uniform buffer, current value}, sparse.
290290
struct SymIntSlot {
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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/TensorMeta.h>
13+
#include <executorch/backends/webgpu/runtime/ops/cat/cat_wgsl.h>
14+
15+
#include <webgpu/webgpu.h>
16+
17+
#include <cstdint>
18+
#include <stdexcept>
19+
#include <vector>
20+
21+
namespace executorch::backends::webgpu {
22+
23+
namespace {
24+
25+
struct CatParams {
26+
uint32_t concat_dim;
27+
uint32_t off_k;
28+
uint32_t _pad[2];
29+
};
30+
static_assert(
31+
sizeof(CatParams) == 16,
32+
"CatParams must match the WGSL Params uniform (16-byte aligned)");
33+
34+
// cat: 1 dispatch/input -> disjoint out slab at host off_k (Vulkan concat).
35+
void cat_impl(WebGPUGraph& graph, const std::vector<int>& args) {
36+
// args: [tensors (ValueList), dim, out].
37+
const int list_id = args.at(0);
38+
const int out_id = args.at(args.size() - 1);
39+
40+
if (graph.get_value_type(list_id) != WebGPUGraph::ValueType::ValueList) {
41+
throw std::runtime_error("cat: tensors arg is not a ValueList");
42+
}
43+
if (graph.get_value_type(args.at(1)) != WebGPUGraph::ValueType::Int) {
44+
throw std::runtime_error("cat: dim arg is not a static Int");
45+
}
46+
if (graph.get_value_type(out_id) != WebGPUGraph::ValueType::Tensor) {
47+
throw std::runtime_error("cat: out arg is not a tensor");
48+
}
49+
50+
WGPUDevice device = graph.device();
51+
const std::vector<int>& ids = graph.get_value_list(list_id);
52+
if (ids.empty()) {
53+
throw std::runtime_error("cat: empty input list");
54+
}
55+
56+
const auto& out_tensor = graph.get_tensor(out_id);
57+
const int ndim = static_cast<int>(out_tensor.dims.size());
58+
59+
int64_t dim = graph.get_int(args.at(1));
60+
if (dim < 0) {
61+
dim += ndim;
62+
}
63+
if (dim < 0 || dim >= ndim) {
64+
throw std::runtime_error("cat: dim out of range");
65+
}
66+
67+
// Workgroup size is invariant across inputs: clamp once, share the constant.
68+
uint32_t wg_size = utils::clamp_workgroup_size(device, kCatWorkgroupSizeX);
69+
70+
// Validate + cache input meta/wgc BEFORE any GPU alloc (no leak on throw).
71+
std::vector<TensorMeta> in_metas(ids.size());
72+
std::vector<uint32_t> wg_counts(ids.size());
73+
int64_t concat_sum = 0;
74+
for (size_t k = 0; k < ids.size(); k++) {
75+
const int id = ids[k];
76+
if (graph.get_value_type(id) != WebGPUGraph::ValueType::Tensor) {
77+
throw std::runtime_error("cat: input list element is not a tensor");
78+
}
79+
const auto& in_tensor = graph.get_tensor(id);
80+
if (static_cast<int>(in_tensor.dims.size()) != ndim) {
81+
throw std::runtime_error("cat: input rank != output rank");
82+
}
83+
for (int d = 0; d < ndim; d++) {
84+
if (d != dim && in_tensor.dims[d] != out_tensor.dims[d]) {
85+
throw std::runtime_error("cat: non-concat dim size mismatch");
86+
}
87+
}
88+
fill_tensor_meta(in_tensor, &in_metas[k]);
89+
if (in_tensor.nbytes !=
90+
static_cast<size_t>(in_metas[k].numel) * sizeof(float)) {
91+
throw std::runtime_error("cat: non-fp32 input (nbytes != numel * 4)");
92+
}
93+
wg_counts[k] = utils::compute_1d_workgroup_count(
94+
device, in_metas[k].numel, wg_size, "cat");
95+
concat_sum += in_tensor.dims[dim];
96+
}
97+
if (concat_sum != out_tensor.dims[dim]) {
98+
throw std::runtime_error("cat: concat dim sizes do not sum to output");
99+
}
100+
101+
TensorMeta out_meta;
102+
fill_tensor_meta(out_tensor, &out_meta);
103+
if (out_tensor.nbytes !=
104+
static_cast<size_t>(out_meta.numel) * sizeof(float)) {
105+
throw std::runtime_error("cat: non-fp32 output (nbytes != numel * 4)");
106+
}
107+
108+
WGPUBuffer out_meta_buf =
109+
utils::make_uniform(device, &out_meta, sizeof(TensorMeta));
110+
graph.add_uniform_buffer_bytes(sizeof(TensorMeta));
111+
112+
WGPUConstantEntry wg_size_constant = {};
113+
wg_size_constant.key = {"wg_size", WGPU_STRLEN};
114+
wg_size_constant.value = static_cast<double>(wg_size);
115+
116+
// Shared shader/layout; fresh pipeline+bind group per input (no double-free).
117+
WGPUShaderSourceWGSL wgsl_desc = {};
118+
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
119+
wgsl_desc.code = {kCatWGSL, WGPU_STRLEN};
120+
WGPUShaderModuleDescriptor shader_desc = {};
121+
shader_desc.nextInChain = &wgsl_desc.chain;
122+
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
123+
124+
WGPUBindGroupLayoutEntry entries[5] = {};
125+
entries[0].binding = 0;
126+
entries[0].visibility = WGPUShaderStage_Compute;
127+
entries[0].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
128+
entries[1].binding = 1;
129+
entries[1].visibility = WGPUShaderStage_Compute;
130+
entries[1].buffer.type = WGPUBufferBindingType_Storage;
131+
entries[2].binding = 2;
132+
entries[2].visibility = WGPUShaderStage_Compute;
133+
entries[2].buffer.type = WGPUBufferBindingType_Uniform;
134+
entries[3].binding = 3;
135+
entries[3].visibility = WGPUShaderStage_Compute;
136+
entries[3].buffer.type = WGPUBufferBindingType_Uniform;
137+
entries[4].binding = 4;
138+
entries[4].visibility = WGPUShaderStage_Compute;
139+
entries[4].buffer.type = WGPUBufferBindingType_Uniform;
140+
141+
WGPUBindGroupLayoutDescriptor bgl_desc = {};
142+
bgl_desc.entryCount = 5;
143+
bgl_desc.entries = entries;
144+
WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
145+
146+
WGPUPipelineLayoutDescriptor pl_desc = {};
147+
pl_desc.bindGroupLayoutCount = 1;
148+
pl_desc.bindGroupLayouts = &bgl;
149+
WGPUPipelineLayout pipeline_layout =
150+
wgpuDeviceCreatePipelineLayout(device, &pl_desc);
151+
152+
uint32_t off_k = 0;
153+
for (size_t k = 0; k < ids.size(); k++) {
154+
const auto& in_tensor = graph.get_tensor(ids[k]);
155+
156+
CatParams params = {};
157+
params.concat_dim = static_cast<uint32_t>(dim);
158+
params.off_k = off_k;
159+
160+
WGPUBuffer in_meta_buf =
161+
utils::make_uniform(device, &in_metas[k], sizeof(TensorMeta));
162+
WGPUBuffer params_buf =
163+
utils::make_uniform(device, &params, sizeof(CatParams));
164+
graph.add_uniform_buffer_bytes(sizeof(TensorMeta) + sizeof(CatParams));
165+
166+
WGPUComputePipelineDescriptor pipeline_desc = {};
167+
pipeline_desc.layout = pipeline_layout;
168+
pipeline_desc.compute.module = shader;
169+
pipeline_desc.compute.entryPoint = {"main", WGPU_STRLEN};
170+
pipeline_desc.compute.constantCount = 1;
171+
pipeline_desc.compute.constants = &wg_size_constant;
172+
WGPUComputePipeline pipeline =
173+
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
174+
175+
WGPUBindGroupEntry bg_entries[5] = {};
176+
bg_entries[0].binding = 0;
177+
bg_entries[0].buffer = in_tensor.buffer;
178+
bg_entries[0].size = in_tensor.nbytes;
179+
bg_entries[1].binding = 1;
180+
bg_entries[1].buffer = out_tensor.buffer;
181+
bg_entries[1].size = out_tensor.nbytes;
182+
bg_entries[2].binding = 2;
183+
bg_entries[2].buffer = out_meta_buf;
184+
bg_entries[2].size = sizeof(TensorMeta);
185+
bg_entries[3].binding = 3;
186+
bg_entries[3].buffer = in_meta_buf;
187+
bg_entries[3].size = sizeof(TensorMeta);
188+
bg_entries[4].binding = 4;
189+
bg_entries[4].buffer = params_buf;
190+
bg_entries[4].size = sizeof(CatParams);
191+
192+
WGPUBindGroupDescriptor bg_desc = {};
193+
bg_desc.layout = bgl;
194+
bg_desc.entryCount = 5;
195+
bg_desc.entries = bg_entries;
196+
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
197+
198+
graph.add_dispatch({pipeline, bind_group, wg_counts[k]});
199+
// Drop our refs; this input's bind group keeps its uniforms alive.
200+
wgpuBufferRelease(in_meta_buf);
201+
wgpuBufferRelease(params_buf);
202+
off_k += static_cast<uint32_t>(in_tensor.dims[dim]);
203+
}
204+
205+
wgpuShaderModuleRelease(shader);
206+
wgpuBindGroupLayoutRelease(bgl);
207+
wgpuPipelineLayoutRelease(pipeline_layout);
208+
// Drop our ref to the shared out_meta; the bind groups keep it alive.
209+
wgpuBufferRelease(out_meta_buf);
210+
}
211+
212+
} // namespace
213+
214+
WEBGPU_REGISTER_OPERATORS {
215+
WEBGPU_REGISTER_OP(aten.cat.default, cat_impl);
216+
}
217+
218+
} // namespace executorch::backends::webgpu
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
@group(0) @binding(0) var<storage, read> input: array<f32>;
2+
@group(0) @binding(1) var<storage, read_write> output: array<f32>;
3+
4+
struct TensorMeta {
5+
ndim: u32,
6+
numel: u32,
7+
sizes: vec4<u32>,
8+
strides: vec4<u32>,
9+
}
10+
@group(0) @binding(2) var<uniform> out_meta: TensorMeta;
11+
@group(0) @binding(3) var<uniform> in_meta: TensorMeta;
12+
13+
struct Params {
14+
concat_dim: u32,
15+
off_k: u32,
16+
}
17+
@group(0) @binding(4) var<uniform> params: Params;
18+
19+
override wg_size: u32 = 64u;
20+
21+
@compute @workgroup_size(wg_size, 1, 1)
22+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
23+
let in_bufi = gid.x;
24+
if (in_bufi >= in_meta.numel) {
25+
return;
26+
}
27+
28+
// Scatter: in coord -> out coord, concat dim shifted by off_k (Vulkan concat).
29+
var rem = in_bufi;
30+
var out_bufi: u32 = 0u;
31+
for (var d: u32 = 0u; d < in_meta.ndim; d = d + 1u) {
32+
let coord = rem / in_meta.strides[d];
33+
rem = rem % in_meta.strides[d];
34+
var out_coord = coord;
35+
if (d == params.concat_dim) {
36+
out_coord = coord + params.off_k;
37+
}
38+
out_bufi = out_bufi + out_coord * out_meta.strides[d];
39+
}
40+
output[out_bufi] = input[in_bufi];
41+
}

0 commit comments

Comments
 (0)