|
| 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 |
0 commit comments