Skip to content

Commit 5dcc278

Browse files
committed
Update
[ghstack-poisoned]
1 parent 716e481 commit 5dcc278

8 files changed

Lines changed: 755 additions & 3 deletions

File tree

backends/webgpu/CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,20 @@ if(EXECUTORCH_BUILD_WEBGPU_PROFILING)
9494
)
9595
endif()
9696

97+
# Split-KV FlashDecoding decode path (sdpa_fd_decode). Default ON: selected at
98+
# runtime for decode (S==1) shapes it supports (head dim <= kSdpaFdMaxHeadDim);
99+
# other shapes use the materialized SDPA path. Set OFF as a kill-switch to drop
100+
# all FlashDecoding code from the build.
101+
option(EXECUTORCH_BUILD_WEBGPU_SDPA_FD
102+
"Enable split-KV FlashDecoding SDPA decode path" ON
103+
)
104+
if(EXECUTORCH_BUILD_WEBGPU_SDPA_FD)
105+
target_sources(
106+
webgpu_backend PRIVATE runtime/ops/sdpa_fd_decode/SdpaFdDecode.cpp
107+
)
108+
target_compile_definitions(webgpu_backend PRIVATE WEBGPU_SDPA_FD)
109+
endif()
110+
97111
# Link with --whole-archive for static registration of backend + ops
98112
executorch_target_link_options_shared_lib(webgpu_backend)
99113

backends/webgpu/runtime/ops/sdpa/Sdpa.cpp

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
#include <executorch/backends/webgpu/runtime/ops/sdpa/sdpa_compute_attn_weights_wgsl.h>
1313
#include <executorch/backends/webgpu/runtime/ops/sdpa/sdpa_compute_out_wgsl.h>
1414
#include <executorch/backends/webgpu/runtime/ops/sdpa/sdpa_softmax_wgsl.h>
15+
#if defined(WEBGPU_SDPA_FD)
16+
#include <executorch/backends/webgpu/runtime/ops/sdpa_fd_decode/SdpaFdDecode.h>
17+
#endif
1518
#include <executorch/backends/webgpu/runtime/ops/update_cache/update_cache_wgsl.h>
1619

1720
#include <webgpu/webgpu.h>
@@ -427,9 +430,6 @@ void sdpa_with_kv_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
427430
static_cast<uint64_t>(S) *
428431
static_cast<uint64_t>(dynamic_pos ? Cmax : context_len);
429432
const uint64_t aw_bytes = aw_cap_floats * sizeof(float);
430-
// Prefill scratch scales as Hq·S·Cmax; can be large for long-context prefill.
431-
WGPUBuffer attn_weights = graph.create_scratch_buffer(aw_bytes);
432-
WGPUBuffer attn_weights_softmax = graph.create_scratch_buffer(aw_bytes);
433433

434434
// Dynamic input_pos: the resize hook rewrites these per step.
435435
WGPUBuffer uc_k_buf = nullptr, uc_v_buf = nullptr, qk_buf = nullptr,
@@ -473,6 +473,20 @@ void sdpa_with_kv_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
473473
dynamic_pos,
474474
"update_cache(V)");
475475

476+
#ifdef WEBGPU_SDPA_FD
477+
// FlashDecoding decode (S==1, static pos). Shapes FD can't handle (head dim
478+
// > kSdpaFdMaxHeadDim) fall through to the materialized path below.
479+
if (S == 1 && !dynamic_pos && D <= kSdpaFdMaxHeadDim) {
480+
sdpa_fd_decode_dispatch(
481+
graph, q, k_cache, v_cache, out, Hq, Hkv, D, context_len, g, scale);
482+
return;
483+
}
484+
#endif
485+
486+
// QK/softmax scratch — allocated only on the non-FD path (Hq*S*Cmax prefill).
487+
WGPUBuffer attn_weights = graph.create_scratch_buffer(aw_bytes);
488+
WGPUBuffer attn_weights_softmax = graph.create_scratch_buffer(aw_bytes);
489+
476490
// --- Dispatch 3: QK -> attn_weights. One thread per TM x TN tile.
477491
{
478492
if (aw_floats > UINT32_MAX) {
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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+
// Split-KV FlashDecoding decode dispatch (split + reduce passes).
10+
11+
#include <executorch/backends/webgpu/runtime/WebGPUGraph.h>
12+
#include <executorch/backends/webgpu/runtime/WebGPUUtils.h>
13+
#include <executorch/backends/webgpu/runtime/ops/sdpa_fd_decode/SdpaFdDecode.h>
14+
#include <executorch/backends/webgpu/runtime/ops/sdpa_fd_decode/sdpa_fd_reduce_wgsl.h>
15+
#include <executorch/backends/webgpu/runtime/ops/sdpa_fd_decode/sdpa_fd_split_wgsl.h>
16+
17+
#include <webgpu/webgpu.h>
18+
19+
#include <cstdint>
20+
#include <cstring>
21+
#include <stdexcept>
22+
23+
namespace executorch::backends::webgpu {
24+
25+
namespace {
26+
27+
// MUST match the .wgsl: MAX_SPLITS and WG_SIZE*MAX_D_PER_LANE.
28+
constexpr uint32_t kSdpaFdSplitTile = 64; // KV positions per split
29+
constexpr uint32_t kSdpaFdMaxSplits = 128; // == MAX_SPLITS in both .wgsl files
30+
// Public head-dim limit (kSdpaFdMaxHeadDim) must equal the kernel's lane-owns-D
31+
// reach; tie them so a WG_SIZE change can't silently desync the Sdpa.cpp gate.
32+
static_assert(
33+
kSdpaFdMaxHeadDim == kSdpaFdSplitWorkgroupSizeX * 2u,
34+
"kSdpaFdMaxHeadDim must match WG_SIZE * MAX_D_PER_LANE");
35+
36+
struct FdSplitParams {
37+
uint32_t Hq;
38+
uint32_t Hkv;
39+
uint32_t D;
40+
uint32_t context_len;
41+
uint32_t g;
42+
uint32_t num_splits;
43+
uint32_t split_len;
44+
float scale;
45+
};
46+
static_assert(sizeof(FdSplitParams) == 32, "FdSplitParams must be 32B");
47+
48+
struct FdReduceParams {
49+
uint32_t D;
50+
uint32_t num_splits;
51+
uint32_t _pad0;
52+
uint32_t _pad1;
53+
};
54+
static_assert(sizeof(FdReduceParams) == 16, "FdReduceParams must be 16B");
55+
56+
struct BufferBinding {
57+
WGPUBuffer buffer;
58+
uint64_t size;
59+
};
60+
61+
WGPUBuffer
62+
make_uniform_buffer(WebGPUGraph& graph, const void* data, size_t size) {
63+
WGPUDevice device = graph.device();
64+
WGPUBufferDescriptor desc = {};
65+
desc.size = size;
66+
desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
67+
desc.mappedAtCreation = true;
68+
WGPUBuffer buffer = wgpuDeviceCreateBuffer(device, &desc);
69+
void* mapped = wgpuBufferGetMappedRange(buffer, 0, size);
70+
std::memcpy(mapped, data, size);
71+
wgpuBufferUnmap(buffer);
72+
graph.add_uniform_buffer_bytes(size);
73+
return buffer;
74+
}
75+
76+
// Mirrors Sdpa.cpp build_dispatch; n_rw leading bindings are read_write.
77+
void build_dispatch(
78+
WebGPUGraph& graph,
79+
const char* wgsl_source,
80+
const BufferBinding* storage_bindings,
81+
uint32_t n_storage,
82+
uint32_t n_rw,
83+
WGPUBuffer uniform_buffer,
84+
uint64_t uniform_size,
85+
uint32_t workgroup_count_x,
86+
const char* kernel_name) {
87+
WGPUDevice device = graph.device();
88+
89+
WGPUShaderSourceWGSL wgsl_desc = {};
90+
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
91+
wgsl_desc.code = {wgsl_source, WGPU_STRLEN};
92+
WGPUShaderModuleDescriptor shader_desc = {};
93+
shader_desc.nextInChain = &wgsl_desc.chain;
94+
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
95+
96+
constexpr uint32_t kMaxEntries = 8;
97+
if (n_storage + 1u > kMaxEntries) {
98+
throw std::runtime_error(
99+
"WebGPU sdpa FlashDecoding: bind group entry count exceeds kMaxEntries");
100+
}
101+
WGPUBindGroupLayoutEntry bgl_entries[kMaxEntries] = {};
102+
const uint32_t uniform_binding = n_storage;
103+
for (uint32_t i = 0; i < n_storage; i++) {
104+
bgl_entries[i].binding = i;
105+
bgl_entries[i].visibility = WGPUShaderStage_Compute;
106+
bgl_entries[i].buffer.type = (i < n_rw)
107+
? WGPUBufferBindingType_Storage
108+
: WGPUBufferBindingType_ReadOnlyStorage;
109+
}
110+
bgl_entries[uniform_binding].binding = uniform_binding;
111+
bgl_entries[uniform_binding].visibility = WGPUShaderStage_Compute;
112+
bgl_entries[uniform_binding].buffer.type = WGPUBufferBindingType_Uniform;
113+
114+
WGPUBindGroupLayoutDescriptor bgl_desc = {};
115+
bgl_desc.entryCount = n_storage + 1;
116+
bgl_desc.entries = bgl_entries;
117+
WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
118+
119+
WGPUPipelineLayoutDescriptor pl_desc = {};
120+
pl_desc.bindGroupLayoutCount = 1;
121+
pl_desc.bindGroupLayouts = &bgl;
122+
WGPUPipelineLayout pipeline_layout =
123+
wgpuDeviceCreatePipelineLayout(device, &pl_desc);
124+
125+
WGPUComputePipelineDescriptor pipeline_desc = {};
126+
pipeline_desc.layout = pipeline_layout;
127+
pipeline_desc.compute.module = shader;
128+
pipeline_desc.compute.entryPoint = {"main", WGPU_STRLEN};
129+
WGPUComputePipeline pipeline =
130+
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
131+
132+
WGPUBindGroupEntry bg_entries[kMaxEntries] = {};
133+
for (uint32_t i = 0; i < n_storage; i++) {
134+
bg_entries[i].binding = i;
135+
bg_entries[i].buffer = storage_bindings[i].buffer;
136+
bg_entries[i].size = storage_bindings[i].size;
137+
}
138+
bg_entries[uniform_binding].binding = uniform_binding;
139+
bg_entries[uniform_binding].buffer = uniform_buffer;
140+
bg_entries[uniform_binding].size = uniform_size;
141+
142+
WGPUBindGroupDescriptor bg_desc = {};
143+
bg_desc.layout = bgl;
144+
bg_desc.entryCount = n_storage + 1;
145+
bg_desc.entries = bg_entries;
146+
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
147+
148+
graph.add_dispatch({pipeline, bind_group, workgroup_count_x, kernel_name});
149+
150+
wgpuShaderModuleRelease(shader);
151+
wgpuBindGroupLayoutRelease(bgl);
152+
wgpuPipelineLayoutRelease(pipeline_layout);
153+
wgpuBufferRelease(uniform_buffer);
154+
}
155+
156+
} // namespace
157+
158+
void sdpa_fd_decode_dispatch(
159+
WebGPUGraph& graph,
160+
const WebGPUTensor& q,
161+
const WebGPUTensor& k_cache,
162+
const WebGPUTensor& v_cache,
163+
const WebGPUTensor& out,
164+
int64_t Hq,
165+
int64_t Hkv,
166+
int64_t D,
167+
int64_t context_len,
168+
int64_t g,
169+
float scale) {
170+
// Defensive contract guard: the Sdpa.cpp gate only routes D <= this here, but
171+
// keep the check (lane-owns-D reach) so a future caller can't silently overrun.
172+
if (D > kSdpaFdMaxHeadDim) {
173+
throw std::runtime_error(
174+
"WebGPU sdpa FlashDecoding: head dim must be <= 128");
175+
}
176+
if (D % 4 != 0) {
177+
throw std::runtime_error(
178+
"WebGPU sdpa FlashDecoding: head dim must be a multiple of 4");
179+
}
180+
181+
// Split factor: one split per kSdpaFdSplitTile KV rows, capped.
182+
uint32_t num_splits = static_cast<uint32_t>(
183+
(context_len + kSdpaFdSplitTile - 1) / kSdpaFdSplitTile);
184+
if (num_splits < 1u) {
185+
num_splits = 1u;
186+
}
187+
if (num_splits > kSdpaFdMaxSplits) {
188+
num_splits = kSdpaFdMaxSplits;
189+
}
190+
const uint32_t split_len =
191+
static_cast<uint32_t>((context_len + num_splits - 1) / num_splits);
192+
193+
// Scratch: per-(head,split) partials at kSdpaFdMaxSplits stride.
194+
const uint64_t po_floats = static_cast<uint64_t>(Hq) *
195+
static_cast<uint64_t>(kSdpaFdMaxSplits) * static_cast<uint64_t>(D);
196+
const uint64_t pml_floats = static_cast<uint64_t>(Hq) *
197+
static_cast<uint64_t>(kSdpaFdMaxSplits) * 2ull;
198+
WGPUBuffer part_o = graph.create_scratch_buffer(po_floats * sizeof(float));
199+
WGPUBuffer part_ml = graph.create_scratch_buffer(pml_floats * sizeof(float));
200+
201+
// Pass 1: split (Hq*num_splits WGs) -> writes part_o, part_ml.
202+
FdSplitParams sp = {};
203+
sp.Hq = static_cast<uint32_t>(Hq);
204+
sp.Hkv = static_cast<uint32_t>(Hkv);
205+
sp.D = static_cast<uint32_t>(D);
206+
sp.context_len = static_cast<uint32_t>(context_len);
207+
sp.g = static_cast<uint32_t>(g);
208+
sp.num_splits = num_splits;
209+
sp.split_len = split_len;
210+
sp.scale = scale;
211+
WGPUBuffer ub_split = make_uniform_buffer(graph, &sp, sizeof(sp));
212+
BufferBinding split_bindings[5] = {
213+
{part_o, po_floats * sizeof(float)},
214+
{part_ml, pml_floats * sizeof(float)},
215+
{q.buffer, q.nbytes},
216+
{k_cache.buffer, k_cache.nbytes},
217+
{v_cache.buffer, v_cache.nbytes}};
218+
const uint32_t wgc_split = utils::compute_1d_workgroup_count(
219+
graph.device(),
220+
static_cast<uint32_t>(Hq) * num_splits * kSdpaFdSplitWorkgroupSizeX,
221+
kSdpaFdSplitWorkgroupSizeX,
222+
"fd_split");
223+
build_dispatch(
224+
graph,
225+
kSdpaFdSplitWGSL,
226+
split_bindings,
227+
5,
228+
2,
229+
ub_split,
230+
sizeof(sp),
231+
wgc_split,
232+
"fd_split");
233+
234+
// Pass 2: reduce (Hq WGs) -> reads part_o, part_ml; writes out.
235+
FdReduceParams rp = {};
236+
rp.D = static_cast<uint32_t>(D);
237+
rp.num_splits = num_splits;
238+
WGPUBuffer ub_reduce = make_uniform_buffer(graph, &rp, sizeof(rp));
239+
BufferBinding reduce_bindings[3] = {
240+
{out.buffer, out.nbytes},
241+
{part_o, po_floats * sizeof(float)},
242+
{part_ml, pml_floats * sizeof(float)}};
243+
const uint32_t wgc_reduce = utils::compute_1d_workgroup_count(
244+
graph.device(),
245+
static_cast<uint32_t>(Hq) * kSdpaFdReduceWorkgroupSizeX,
246+
kSdpaFdReduceWorkgroupSizeX,
247+
"fd_reduce");
248+
build_dispatch(
249+
graph,
250+
kSdpaFdReduceWGSL,
251+
reduce_bindings,
252+
3,
253+
1,
254+
ub_reduce,
255+
sizeof(rp),
256+
wgc_reduce,
257+
"fd_reduce");
258+
}
259+
260+
} // namespace executorch::backends::webgpu
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 <executorch/backends/webgpu/runtime/WebGPUGraph.h>
12+
13+
#include <cstdint>
14+
15+
namespace executorch::backends::webgpu {
16+
17+
// FlashDecoding's lane-owns-D layout covers head dims up to WG_SIZE(64) *
18+
// MAX_D_PER_LANE(2). Decode shapes above this fall through to the materialized
19+
// SDPA path (the FD selection predicate in Sdpa.cpp checks this).
20+
constexpr int64_t kSdpaFdMaxHeadDim = 128;
21+
22+
// Split-KV FlashDecoding decode dispatch (S==1): a split pass over
23+
// Hq*num_splits workgroups + a reduce pass over Hq workgroups. Called from the
24+
// Sdpa.cpp WEBGPU_SDPA_FD branch.
25+
void sdpa_fd_decode_dispatch(
26+
WebGPUGraph& graph,
27+
const WebGPUTensor& q,
28+
const WebGPUTensor& k_cache,
29+
const WebGPUTensor& v_cache,
30+
const WebGPUTensor& out,
31+
int64_t Hq,
32+
int64_t Hkv,
33+
int64_t D,
34+
int64_t context_len,
35+
int64_t g,
36+
float scale);
37+
38+
} // namespace executorch::backends::webgpu

0 commit comments

Comments
 (0)