Skip to content

Commit d846043

Browse files
committed
[ExecuTorch][WebGPU] Add et_vk.fused_ce (fused cross-entropy) to the WebGPU backend
Pull Request resolved: #20933 Add `et_vk.fused_ce` — a fused cross-entropy (loss + dlogits in one op) for the on-device training tail. Key changes: - `runtime/ops/fused_ce/` — fused-CE WGSL kernel (+ the `reduce` shader it depends on) + handler - `custom_ops_lib.py` — `fused_ce` custom-op def + `register_autograd` - `op_registry.py` — `OpFeatures` - `CMakeLists.txt` `WEBGPU_SRCS` — wire the source Training-only, WebGPU-only custom op registered under the shared Vulkan partitioner; no Vulkan kernel yet. Co-authored-with: Claude Code. ghstack-source-id: 405026078 @exported-using-ghexport Differential Revision: [D111755132](https://our.internmc.facebook.com/intern/diff/D111755132/)
1 parent bc1d5e0 commit d846043

6 files changed

Lines changed: 522 additions & 0 deletions

File tree

backends/vulkan/custom_ops_lib.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,56 @@ def rms_norm_impl(
11531153
rms_norm_op = getattr(getattr(torch.ops, namespace), name)
11541154

11551155

1156+
########################
1157+
## fused_ce (training) ##
1158+
########################
1159+
1160+
1161+
def fused_ce_impl(
1162+
logits: torch.Tensor,
1163+
labels: torch.Tensor,
1164+
n_valid: float,
1165+
) -> tuple[torch.Tensor, torch.Tensor]:
1166+
mask = labels >= 0
1167+
safe = labels.clamp(min=0).long()
1168+
lse = torch.logsumexp(logits, dim=-1)
1169+
picked = logits.gather(-1, safe[:, None]).squeeze(-1)
1170+
loss = torch.where(mask, (lse - picked) / n_valid, torch.zeros_like(lse)).sum()
1171+
softmax = torch.softmax(logits, dim=-1)
1172+
onehot = torch.nn.functional.one_hot(safe, logits.shape[-1]).to(logits.dtype)
1173+
dlogits = torch.where(
1174+
mask[:, None], (softmax - onehot) / n_valid, torch.zeros_like(softmax)
1175+
)
1176+
return loss, dlogits
1177+
1178+
1179+
def fused_ce_meta(
1180+
logits: torch.Tensor,
1181+
labels: torch.Tensor,
1182+
n_valid: float,
1183+
) -> tuple[torch.Tensor, torch.Tensor]:
1184+
return logits.new_empty([]), torch.empty_like(logits)
1185+
1186+
1187+
def fused_ce_setup_context(ctx, inputs, output) -> None:
1188+
ctx.save_for_backward(output[1])
1189+
1190+
1191+
def fused_ce_backward(ctx, grad_loss, grad_dlogits):
1192+
(dlogits,) = ctx.saved_tensors
1193+
return grad_loss * dlogits, None, None
1194+
1195+
1196+
name = "fused_ce"
1197+
lib.define(f"{name}(Tensor logits, Tensor labels, float n_valid) -> (Tensor, Tensor)")
1198+
lib.impl(name, fused_ce_impl, "CompositeExplicitAutograd")
1199+
lib.impl(name, fused_ce_meta, "Meta")
1200+
torch.library.register_autograd(
1201+
f"{namespace}::{name}", fused_ce_backward, setup_context=fused_ce_setup_context
1202+
)
1203+
fused_ce_op = getattr(getattr(torch.ops, namespace), name)
1204+
1205+
11561206
# STE weight gradient d_out^T @ x through the frozen 4-bit linear_q4gsw base.
11571207
def linear_dW_impl(
11581208
d_out: torch.Tensor,

backends/vulkan/op_registry.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1755,6 +1755,20 @@ def register_rms_norm():
17551755
)
17561756

17571757

1758+
# =============================================================================
1759+
# FusedCe.cpp (training)
1760+
# =============================================================================
1761+
1762+
1763+
@update_features(exir_ops.edge.et_vk.fused_ce.default)
1764+
def register_fused_ce():
1765+
return OpFeatures(
1766+
inputs_storage=utils.CONTIGUOUS_ANY,
1767+
inputs_dtypes=[utils.FP_T, utils.INT_T, utils.NONE_T],
1768+
outputs_dtypes=[utils.FP_T, utils.FP_T],
1769+
)
1770+
1771+
17581772
@update_features(
17591773
[
17601774
exir_ops.edge.aten.ne.Scalar,

backends/webgpu/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ set(WEBGPU_SRCS
5454
runtime/ops/index/Index.cpp
5555
runtime/ops/sdpa_fd_decode/SdpaFdDecode.cpp
5656
runtime/ops/mm/Mm.cpp
57+
runtime/ops/fused_ce/FusedCe.cpp
5758
runtime/ops/log_softmax/LogSoftmax.cpp
5859
runtime/ops/softmax/Softmax.cpp
5960
runtime/ops/bmm/Bmm.cpp
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
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/fused_ce/fused_ce_wgsl.h>
13+
#include <executorch/backends/webgpu/runtime/ops/reduce/reduce_wgsl.h>
14+
15+
#include <webgpu/webgpu.h>
16+
17+
#include <cstdint>
18+
#include <cstring>
19+
#include <stdexcept>
20+
21+
namespace executorch::backends::webgpu {
22+
23+
namespace {
24+
25+
// Uniform layout matching the fused_ce.wgsl Params struct (16-byte aligned).
26+
struct FusedCeParams {
27+
uint32_t vocab;
28+
uint32_t n_rows;
29+
float n_valid;
30+
float _pad0;
31+
};
32+
static_assert(sizeof(FusedCeParams) == 16, "FusedCeParams must be 16 bytes");
33+
34+
// Mirror reduce.wgsl Params (file-local in Reduce.cpp; re-declared here).
35+
struct ReduceParams {
36+
uint32_t outer;
37+
uint32_t r;
38+
uint32_t inner;
39+
uint32_t is_mean;
40+
};
41+
static_assert(sizeof(ReduceParams) == 16, "ReduceParams must be 16 bytes");
42+
43+
WGPUShaderModule make_shader(WGPUDevice device, const char* wgsl) {
44+
WGPUShaderSourceWGSL wgsl_desc = {};
45+
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
46+
wgsl_desc.code = {wgsl, WGPU_STRLEN};
47+
WGPUShaderModuleDescriptor shader_desc = {};
48+
shader_desc.nextInChain = &wgsl_desc.chain;
49+
return wgpuDeviceCreateShaderModule(device, &shader_desc);
50+
}
51+
52+
WGPUBuffer create_uniform(
53+
WebGPUGraph& graph,
54+
WGPUDevice device,
55+
const void* data,
56+
size_t size) {
57+
WGPUBufferDescriptor desc = {};
58+
desc.size = size;
59+
desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
60+
desc.mappedAtCreation = true;
61+
WGPUBuffer buffer = wgpuDeviceCreateBuffer(device, &desc);
62+
std::memcpy(wgpuBufferGetMappedRange(buffer, 0, size), data, size);
63+
wgpuBufferUnmap(buffer);
64+
graph.add_uniform_buffer_bytes(size);
65+
return buffer;
66+
}
67+
68+
// out valuelist packs the 2-tuple (loss, dlogits) as one id.
69+
void fused_ce_impl(WebGPUGraph& graph, const std::vector<int>& args) {
70+
const int logits_id = args.at(0);
71+
const int labels_id = args.at(1);
72+
const int n_valid_id = args.at(2);
73+
const std::vector<int>& outs = graph.get_value_list(args.at(3));
74+
if (outs.size() != 2) {
75+
throw std::runtime_error(
76+
"WebGPU fused_ce: expected 2 outputs (loss, dlogits)");
77+
}
78+
const int loss_id = outs.at(0);
79+
const int dlogits_id = outs.at(1);
80+
81+
WGPUDevice device = graph.device();
82+
const auto& logits = graph.get_tensor(logits_id);
83+
const auto& labels = graph.get_tensor(labels_id);
84+
const auto& dlogits = graph.get_tensor(dlogits_id);
85+
const auto& loss = graph.get_tensor(loss_id);
86+
87+
if (logits.dims.size() != 2) {
88+
throw std::runtime_error("WebGPU fused_ce: logits must be 2D [N, V]");
89+
}
90+
const uint64_t n_rows = static_cast<uint64_t>(logits.dims[0]);
91+
const uint64_t vocab = static_cast<uint64_t>(logits.dims[1]);
92+
const uint64_t numel = n_rows * vocab;
93+
94+
if (dlogits.dims != logits.dims) {
95+
throw std::runtime_error(
96+
"WebGPU fused_ce: dlogits shape must match logits");
97+
}
98+
if (logits.nbytes != numel * sizeof(float) ||
99+
dlogits.nbytes != numel * sizeof(float)) {
100+
throw std::runtime_error("WebGPU fused_ce: logits/dlogits fp32-only");
101+
}
102+
if (labels.nbytes != n_rows * sizeof(int32_t)) {
103+
throw std::runtime_error("WebGPU fused_ce: labels must be int32 [N]");
104+
}
105+
if (loss.nbytes != sizeof(float)) {
106+
throw std::runtime_error("WebGPU fused_ce: loss must be a scalar [1]");
107+
}
108+
if (graph.get_value_type(n_valid_id) != WebGPUGraph::ValueType::Double) {
109+
throw std::runtime_error("WebGPU fused_ce: n_valid must be a float scalar");
110+
}
111+
const double n_valid = graph.get_double(n_valid_id);
112+
if (n_valid <= 0.0) {
113+
throw std::runtime_error("WebGPU fused_ce: n_valid must be positive");
114+
}
115+
if (n_rows > utils::queried_max_workgroups(device)) {
116+
throw std::runtime_error("WebGPU fused_ce: n_rows exceeds dispatch limit");
117+
}
118+
119+
WGPUBuffer loss_partial = graph.create_scratch_buffer(n_rows * sizeof(float));
120+
121+
// one workgroup per row
122+
FusedCeParams ce_params = {};
123+
ce_params.vocab = static_cast<uint32_t>(vocab);
124+
ce_params.n_rows = static_cast<uint32_t>(n_rows);
125+
ce_params.n_valid = static_cast<float>(n_valid);
126+
const uint32_t ce_wg =
127+
utils::clamp_workgroup_size(device, kFusedCeWorkgroupSizeX);
128+
WGPUBuffer ce_uniform =
129+
create_uniform(graph, device, &ce_params, sizeof(ce_params));
130+
WGPUShaderModule ce_shader = make_shader(device, kFusedCeWGSL);
131+
132+
WGPUBindGroupLayoutEntry ce_entries[5] = {};
133+
ce_entries[0].binding = 0;
134+
ce_entries[0].visibility = WGPUShaderStage_Compute;
135+
ce_entries[0].buffer.type = WGPUBufferBindingType_Storage;
136+
ce_entries[1].binding = 1;
137+
ce_entries[1].visibility = WGPUShaderStage_Compute;
138+
ce_entries[1].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
139+
ce_entries[2].binding = 2;
140+
ce_entries[2].visibility = WGPUShaderStage_Compute;
141+
ce_entries[2].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
142+
ce_entries[3].binding = 3;
143+
ce_entries[3].visibility = WGPUShaderStage_Compute;
144+
ce_entries[3].buffer.type = WGPUBufferBindingType_Storage;
145+
ce_entries[4].binding = 4;
146+
ce_entries[4].visibility = WGPUShaderStage_Compute;
147+
ce_entries[4].buffer.type = WGPUBufferBindingType_Uniform;
148+
149+
WGPUBindGroupLayoutDescriptor ce_bgl_desc = {};
150+
ce_bgl_desc.entryCount = 5;
151+
ce_bgl_desc.entries = ce_entries;
152+
WGPUBindGroupLayout ce_bgl =
153+
wgpuDeviceCreateBindGroupLayout(device, &ce_bgl_desc);
154+
155+
WGPUPipelineLayoutDescriptor ce_pl_desc = {};
156+
ce_pl_desc.bindGroupLayoutCount = 1;
157+
ce_pl_desc.bindGroupLayouts = &ce_bgl;
158+
WGPUPipelineLayout ce_pl =
159+
wgpuDeviceCreatePipelineLayout(device, &ce_pl_desc);
160+
161+
WGPUConstantEntry ce_wg_const = {};
162+
ce_wg_const.key = {"wg_size", WGPU_STRLEN};
163+
ce_wg_const.value = static_cast<double>(ce_wg);
164+
165+
WGPUComputePipelineDescriptor ce_pipe_desc = {};
166+
ce_pipe_desc.layout = ce_pl;
167+
ce_pipe_desc.compute.module = ce_shader;
168+
ce_pipe_desc.compute.entryPoint = {"main", WGPU_STRLEN};
169+
ce_pipe_desc.compute.constantCount = 1;
170+
ce_pipe_desc.compute.constants = &ce_wg_const;
171+
WGPUComputePipeline ce_pipe =
172+
wgpuDeviceCreateComputePipeline(device, &ce_pipe_desc);
173+
174+
WGPUBindGroupEntry ce_bg[5] = {};
175+
ce_bg[0].binding = 0;
176+
ce_bg[0].buffer = dlogits.buffer;
177+
ce_bg[0].size = dlogits.nbytes;
178+
ce_bg[1].binding = 1;
179+
ce_bg[1].buffer = logits.buffer;
180+
ce_bg[1].size = logits.nbytes;
181+
ce_bg[2].binding = 2;
182+
ce_bg[2].buffer = labels.buffer;
183+
ce_bg[2].size = labels.nbytes;
184+
ce_bg[3].binding = 3;
185+
ce_bg[3].buffer = loss_partial;
186+
ce_bg[3].size = n_rows * sizeof(float);
187+
ce_bg[4].binding = 4;
188+
ce_bg[4].buffer = ce_uniform;
189+
ce_bg[4].size = sizeof(ce_params);
190+
191+
WGPUBindGroupDescriptor ce_bg_desc = {};
192+
ce_bg_desc.layout = ce_bgl;
193+
ce_bg_desc.entryCount = 5;
194+
ce_bg_desc.entries = ce_bg;
195+
WGPUBindGroup ce_bind_group = wgpuDeviceCreateBindGroup(device, &ce_bg_desc);
196+
197+
graph.add_dispatch(
198+
{ce_pipe, ce_bind_group, static_cast<uint32_t>(n_rows), "fused_ce"});
199+
200+
wgpuShaderModuleRelease(ce_shader);
201+
wgpuBindGroupLayoutRelease(ce_bgl);
202+
wgpuPipelineLayoutRelease(ce_pl);
203+
wgpuBufferRelease(ce_uniform);
204+
205+
// reduce loss_partial[N] -> loss[1] (reuses reduce.wgsl)
206+
ReduceParams r_params = {};
207+
r_params.outer = 1u;
208+
r_params.r = static_cast<uint32_t>(n_rows);
209+
r_params.inner = 1u;
210+
r_params.is_mean = 0u; // mean already folded into loss_partial
211+
const uint32_t r_wg =
212+
utils::clamp_workgroup_size(device, kReduceWorkgroupSizeX);
213+
const uint32_t r_wgc =
214+
utils::compute_1d_workgroup_count(device, 1u, r_wg, "fused_ce_reduce");
215+
WGPUBuffer r_uniform =
216+
create_uniform(graph, device, &r_params, sizeof(r_params));
217+
WGPUShaderModule r_shader = make_shader(device, kReduceWGSL);
218+
219+
WGPUBindGroupLayoutEntry r_entries[3] = {};
220+
r_entries[0].binding = 0;
221+
r_entries[0].visibility = WGPUShaderStage_Compute;
222+
r_entries[0].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
223+
r_entries[1].binding = 1;
224+
r_entries[1].visibility = WGPUShaderStage_Compute;
225+
r_entries[1].buffer.type = WGPUBufferBindingType_Storage;
226+
r_entries[2].binding = 2;
227+
r_entries[2].visibility = WGPUShaderStage_Compute;
228+
r_entries[2].buffer.type = WGPUBufferBindingType_Uniform;
229+
230+
WGPUBindGroupLayoutDescriptor r_bgl_desc = {};
231+
r_bgl_desc.entryCount = 3;
232+
r_bgl_desc.entries = r_entries;
233+
WGPUBindGroupLayout r_bgl =
234+
wgpuDeviceCreateBindGroupLayout(device, &r_bgl_desc);
235+
236+
WGPUPipelineLayoutDescriptor r_pl_desc = {};
237+
r_pl_desc.bindGroupLayoutCount = 1;
238+
r_pl_desc.bindGroupLayouts = &r_bgl;
239+
WGPUPipelineLayout r_pl = wgpuDeviceCreatePipelineLayout(device, &r_pl_desc);
240+
241+
WGPUConstantEntry r_wg_const = {};
242+
r_wg_const.key = {"wg_size", WGPU_STRLEN};
243+
r_wg_const.value = static_cast<double>(r_wg);
244+
245+
WGPUComputePipelineDescriptor r_pipe_desc = {};
246+
r_pipe_desc.layout = r_pl;
247+
r_pipe_desc.compute.module = r_shader;
248+
r_pipe_desc.compute.entryPoint = {"main", WGPU_STRLEN};
249+
r_pipe_desc.compute.constantCount = 1;
250+
r_pipe_desc.compute.constants = &r_wg_const;
251+
WGPUComputePipeline r_pipe =
252+
wgpuDeviceCreateComputePipeline(device, &r_pipe_desc);
253+
254+
WGPUBindGroupEntry r_bg[3] = {};
255+
r_bg[0].binding = 0;
256+
r_bg[0].buffer = loss_partial;
257+
r_bg[0].size = n_rows * sizeof(float);
258+
r_bg[1].binding = 1;
259+
r_bg[1].buffer = loss.buffer;
260+
r_bg[1].size = loss.nbytes;
261+
r_bg[2].binding = 2;
262+
r_bg[2].buffer = r_uniform;
263+
r_bg[2].size = sizeof(r_params);
264+
265+
WGPUBindGroupDescriptor r_bg_desc = {};
266+
r_bg_desc.layout = r_bgl;
267+
r_bg_desc.entryCount = 3;
268+
r_bg_desc.entries = r_bg;
269+
WGPUBindGroup r_bind_group = wgpuDeviceCreateBindGroup(device, &r_bg_desc);
270+
271+
graph.add_dispatch({r_pipe, r_bind_group, r_wgc, "fused_ce_reduce"});
272+
273+
wgpuShaderModuleRelease(r_shader);
274+
wgpuBindGroupLayoutRelease(r_bgl);
275+
wgpuPipelineLayoutRelease(r_pl);
276+
wgpuBufferRelease(r_uniform);
277+
}
278+
279+
} // namespace
280+
281+
WEBGPU_REGISTER_OPERATORS {
282+
WEBGPU_REGISTER_OP(et_vk.fused_ce.default, fused_ce_impl);
283+
}
284+
285+
} // namespace executorch::backends::webgpu

0 commit comments

Comments
 (0)