Skip to content

Commit 6368226

Browse files
committed
[ExecuTorch][WebGPU] Add softmax ops (_softmax, _log_softmax) to the WebGPU backend
Pull Request resolved: #20919 Add `aten._softmax` + `aten._log_softmax` handlers (max-stable online softmax) for the training tail. Key changes: - `runtime/ops/{softmax,log_softmax}/` — max-stable softmax + log_softmax WGSL kernels + handlers - `CMakeLists.txt` `WEBGPU_SRCS` — wire the sources Reuses the shared Vulkan partitioner (`aten._softmax`/`_log_softmax` already registered); WebGPU kernels only. Co-authored-with: Claude Code. ghstack-source-id: 405026047 @exported-using-ghexport Differential Revision: [D111755118](https://our.internmc.facebook.com/intern/diff/D111755118/)
1 parent f5cb52e commit 6368226

7 files changed

Lines changed: 632 additions & 0 deletions

File tree

backends/webgpu/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ set(WEBGPU_SRCS
5353
runtime/ops/index/Index.cpp
5454
runtime/ops/sdpa_fd_decode/SdpaFdDecode.cpp
5555
runtime/ops/mm/Mm.cpp
56+
runtime/ops/log_softmax/LogSoftmax.cpp
57+
runtime/ops/softmax/Softmax.cpp
5658
runtime/ops/bmm/Bmm.cpp
5759
runtime/ops/linear/Linear.cpp
5860
)
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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/softmax/log_softmax_wgsl.h>
13+
14+
#include <webgpu/webgpu.h>
15+
16+
#include <cstdint>
17+
#include <cstring>
18+
#include <stdexcept>
19+
#include <vector>
20+
21+
namespace executorch::backends::webgpu {
22+
23+
namespace {
24+
25+
// Uniform layout matching the WGSL Params struct (16-byte aligned).
26+
struct LogSoftmaxParams {
27+
uint32_t outer;
28+
uint32_t r;
29+
uint32_t inner;
30+
uint32_t pad_;
31+
};
32+
static_assert(
33+
sizeof(LogSoftmaxParams) == 16,
34+
"LogSoftmaxParams must be 16 bytes");
35+
36+
// Decompose dims into [outer, R, inner] for a reduction along `dim`.
37+
void decompose(
38+
const std::vector<int64_t>& dims,
39+
int64_t dim,
40+
uint32_t& outer,
41+
uint32_t& r,
42+
uint32_t& inner) {
43+
const int64_t ndim = static_cast<int64_t>(dims.size());
44+
if (dim < 0) {
45+
dim += ndim;
46+
}
47+
if (ndim == 0 || dim < 0 || dim >= ndim) {
48+
throw std::runtime_error("WebGPU log_softmax: dim out of range");
49+
}
50+
uint64_t o = 1, in = 1;
51+
for (int64_t d = 0; d < dim; ++d) {
52+
o *= static_cast<uint64_t>(dims[d]);
53+
}
54+
for (int64_t d = dim + 1; d < ndim; ++d) {
55+
in *= static_cast<uint64_t>(dims[d]);
56+
}
57+
outer = static_cast<uint32_t>(o);
58+
r = static_cast<uint32_t>(dims[dim]);
59+
inner = static_cast<uint32_t>(in);
60+
}
61+
62+
void log_softmax_impl(WebGPUGraph& graph, const std::vector<int>& args) {
63+
const int in_id = args.at(0);
64+
const int dim_id = args.at(1);
65+
const int out_id = args.at(3);
66+
67+
WGPUDevice device = graph.device();
68+
69+
const auto& in = graph.get_tensor(in_id);
70+
const auto& out = graph.get_tensor(out_id);
71+
72+
if (in.dims.empty()) {
73+
throw std::runtime_error("WebGPU log_softmax: scalar input unsupported");
74+
}
75+
if (graph.get_value_type(dim_id) != WebGPUGraph::ValueType::Int) {
76+
throw std::runtime_error("WebGPU log_softmax: dim must be an int");
77+
}
78+
const int64_t dim = graph.get_int(dim_id);
79+
80+
uint32_t outer = 0, r = 0, inner = 0;
81+
decompose(in.dims, dim, outer, r, inner);
82+
if (outer == 0 || r == 0 || inner == 0) {
83+
throw std::runtime_error("WebGPU log_softmax: zero-sized reduction");
84+
}
85+
86+
uint64_t numel = 1;
87+
for (int64_t d : in.dims) {
88+
numel *= static_cast<uint64_t>(d);
89+
}
90+
if (in.nbytes != numel * sizeof(float) ||
91+
out.nbytes != numel * sizeof(float)) {
92+
throw std::runtime_error(
93+
"WebGPU log_softmax: fp32-only (byte-size mismatch)");
94+
}
95+
96+
const uint64_t lines = static_cast<uint64_t>(outer) * inner;
97+
if (lines > UINT32_MAX) {
98+
throw std::runtime_error(
99+
"WebGPU log_softmax: line count exceeds dispatch limit");
100+
}
101+
102+
const uint32_t wg_size =
103+
utils::clamp_workgroup_size(device, kLogSoftmaxWorkgroupSizeX);
104+
const uint32_t workgroup_count = utils::compute_1d_workgroup_count(
105+
device, static_cast<uint32_t>(lines), wg_size, "log_softmax");
106+
107+
LogSoftmaxParams params = {};
108+
params.outer = outer;
109+
params.r = r;
110+
params.inner = inner;
111+
112+
WGPUBufferDescriptor uniform_desc = {};
113+
uniform_desc.size = sizeof(LogSoftmaxParams);
114+
uniform_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
115+
uniform_desc.mappedAtCreation = true;
116+
WGPUBuffer uniform_buffer = wgpuDeviceCreateBuffer(device, &uniform_desc);
117+
void* mapped =
118+
wgpuBufferGetMappedRange(uniform_buffer, 0, sizeof(LogSoftmaxParams));
119+
std::memcpy(mapped, &params, sizeof(LogSoftmaxParams));
120+
wgpuBufferUnmap(uniform_buffer);
121+
graph.add_uniform_buffer_bytes(sizeof(LogSoftmaxParams));
122+
123+
WGPUShaderSourceWGSL wgsl_desc = {};
124+
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
125+
wgsl_desc.code = {kLogSoftmaxWGSL, WGPU_STRLEN};
126+
WGPUShaderModuleDescriptor shader_desc = {};
127+
shader_desc.nextInChain = &wgsl_desc.chain;
128+
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
129+
130+
WGPUBindGroupLayoutEntry entries[3] = {};
131+
entries[0].binding = 0;
132+
entries[0].visibility = WGPUShaderStage_Compute;
133+
entries[0].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
134+
entries[1].binding = 1;
135+
entries[1].visibility = WGPUShaderStage_Compute;
136+
entries[1].buffer.type = WGPUBufferBindingType_Storage;
137+
entries[2].binding = 2;
138+
entries[2].visibility = WGPUShaderStage_Compute;
139+
entries[2].buffer.type = WGPUBufferBindingType_Uniform;
140+
141+
WGPUBindGroupLayoutDescriptor bgl_desc = {};
142+
bgl_desc.entryCount = 3;
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+
WGPUConstantEntry wg_size_constant = {};
153+
wg_size_constant.key = {"wg_size", WGPU_STRLEN};
154+
wg_size_constant.value = static_cast<double>(wg_size);
155+
156+
WGPUComputePipelineDescriptor pipeline_desc = {};
157+
pipeline_desc.layout = pipeline_layout;
158+
pipeline_desc.compute.module = shader;
159+
pipeline_desc.compute.entryPoint = {"main", WGPU_STRLEN};
160+
pipeline_desc.compute.constantCount = 1;
161+
pipeline_desc.compute.constants = &wg_size_constant;
162+
WGPUComputePipeline pipeline =
163+
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
164+
165+
WGPUBindGroupEntry bg_entries[3] = {};
166+
bg_entries[0].binding = 0;
167+
bg_entries[0].buffer = in.buffer;
168+
bg_entries[0].size = in.nbytes;
169+
bg_entries[1].binding = 1;
170+
bg_entries[1].buffer = out.buffer;
171+
bg_entries[1].size = out.nbytes;
172+
bg_entries[2].binding = 2;
173+
bg_entries[2].buffer = uniform_buffer;
174+
bg_entries[2].size = sizeof(LogSoftmaxParams);
175+
176+
WGPUBindGroupDescriptor bg_desc = {};
177+
bg_desc.layout = bgl;
178+
bg_desc.entryCount = 3;
179+
bg_desc.entries = bg_entries;
180+
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
181+
182+
const size_t dispatch_idx = graph.add_dispatch(
183+
{pipeline, bind_group, workgroup_count, "log_softmax"});
184+
185+
// Dynamic shapes: recompute the decomposition for the reduced dim + dispatch.
186+
WGPUBuffer params_buf = uniform_buffer;
187+
const uint64_t build_numel = static_cast<uint64_t>(outer) * r * inner;
188+
graph.add_tensor_resize_hook(
189+
in_id,
190+
[in_id, out_id, dim, build_numel, wg_size, dispatch_idx, params_buf](
191+
WebGPUGraph& g) {
192+
const auto& d = g.cur_dims(in_id);
193+
uint32_t o = 0, rr = 0, n = 0;
194+
decompose(std::vector<int64_t>(d.begin(), d.end()), dim, o, rr, n);
195+
if (o == 0u || rr == 0u || n == 0u) {
196+
throw std::runtime_error(
197+
"WebGPU log_softmax: live zero-sized reduction");
198+
}
199+
if (static_cast<uint64_t>(o) * rr * n > build_numel) {
200+
throw std::runtime_error(
201+
"WebGPU log_softmax: live numel exceeds build max");
202+
}
203+
const uint64_t live_lines = static_cast<uint64_t>(o) * n;
204+
LogSoftmaxParams p = {};
205+
p.outer = o;
206+
p.r = rr;
207+
p.inner = n;
208+
wgpuQueueWriteBuffer(g.queue(), params_buf, 0, &p, sizeof(p));
209+
g.dispatch_at(dispatch_idx).workgroup_count_x =
210+
utils::compute_1d_workgroup_count(
211+
g.device(),
212+
static_cast<uint32_t>(live_lines),
213+
wg_size,
214+
"log_softmax(resize)");
215+
g.set_cur_dims(out_id, std::vector<int64_t>(d.begin(), d.end()));
216+
});
217+
218+
wgpuShaderModuleRelease(shader);
219+
wgpuBindGroupLayoutRelease(bgl);
220+
wgpuPipelineLayoutRelease(pipeline_layout);
221+
// Graph owns it so the resize hook can rewrite it; freed in the dtor.
222+
graph.own_uniform_buffer(uniform_buffer);
223+
}
224+
225+
} // namespace
226+
227+
WEBGPU_REGISTER_OPERATORS {
228+
WEBGPU_REGISTER_OP(aten._log_softmax.default, log_softmax_impl);
229+
}
230+
231+
} // namespace executorch::backends::webgpu

0 commit comments

Comments
 (0)