Skip to content

Commit 2647a36

Browse files
authored
[ExecuTorch][WebGPU] Add update_cache op (llama.update_cache)
Differential Revision: D107547308 Pull Request resolved: #20083
1 parent a351a72 commit 2647a36

4 files changed

Lines changed: 271 additions & 0 deletions

File tree

backends/webgpu/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ set(WEBGPU_SRCS
3333
runtime/ops/OperatorRegistry.cpp
3434
runtime/ops/add/BinaryOp.cpp
3535
runtime/ops/rms_norm/RmsNorm.cpp
36+
runtime/ops/update_cache/UpdateCache.cpp
3637
)
3738

3839
add_library(webgpu_backend ${WEBGPU_SRCS})
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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/update_cache/update_cache_wgsl.h>
13+
14+
#include <webgpu/webgpu.h>
15+
16+
#include <cstdint>
17+
#include <cstring>
18+
#include <stdexcept>
19+
20+
namespace executorch::backends::webgpu {
21+
22+
namespace {
23+
24+
// Uniform buffer layout matching the WGSL Params struct (16-byte aligned).
25+
struct UpdateCacheParams {
26+
uint32_t numel;
27+
uint32_t dst_offset;
28+
uint32_t cache_numel;
29+
uint32_t _pad0;
30+
};
31+
static_assert(
32+
sizeof(UpdateCacheParams) == 16,
33+
"UpdateCacheParams must be 16 bytes");
34+
35+
// llama.update_cache.default args: [value, cache, input_pos, out].
36+
void update_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
37+
const int value_id = args.at(0);
38+
const int cache_id = args.at(1);
39+
const int input_pos_id = args.at(2);
40+
41+
WGPUDevice device = graph.device();
42+
43+
const auto& value_tensor = graph.get_tensor(value_id);
44+
const auto& cache_tensor = graph.get_tensor(cache_id);
45+
if (value_tensor.dims.size() < 4 || cache_tensor.dims.size() < 4 ||
46+
value_tensor.nbytes == 0) {
47+
throw std::runtime_error("WebGPU update_cache: expects 4D value and cache");
48+
}
49+
50+
uint64_t value_numel = 1;
51+
for (int64_t d : value_tensor.dims) {
52+
value_numel *= static_cast<uint64_t>(d);
53+
}
54+
// fp32-only shader: bail if bytes don't match an fp32 element count.
55+
if (value_tensor.nbytes != value_numel * sizeof(float)) {
56+
throw std::runtime_error(
57+
"WebGPU update_cache: fp32-only (byte-size mismatch)");
58+
}
59+
60+
const size_t ndim = value_tensor.dims.size();
61+
const size_t cndim = cache_tensor.dims.size();
62+
// Mirror Vulkan update_cache_impl shape guards (backends/vulkan SDPA.cpp).
63+
if (value_tensor.dims[ndim - 4] != 1 || cache_tensor.dims[cndim - 4] != 1) {
64+
throw std::runtime_error("WebGPU update_cache: batch must be 1");
65+
}
66+
if (value_tensor.dims[ndim - 1] != cache_tensor.dims[cndim - 1]) {
67+
throw std::runtime_error("WebGPU update_cache: head_dim mismatch");
68+
}
69+
if (value_tensor.dims[ndim - 2] != cache_tensor.dims[cndim - 2]) {
70+
throw std::runtime_error("WebGPU update_cache: n_heads mismatch");
71+
}
72+
const uint64_t head_dim = static_cast<uint64_t>(value_tensor.dims[ndim - 1]);
73+
const uint64_t n_heads = static_cast<uint64_t>(value_tensor.dims[ndim - 2]);
74+
75+
uint64_t cache_numel = 1;
76+
for (int64_t d : cache_tensor.dims) {
77+
cache_numel *= static_cast<uint64_t>(d);
78+
}
79+
80+
if (graph.get_value_type(input_pos_id) != WebGPUGraph::ValueType::Int) {
81+
throw std::runtime_error(
82+
"WebGPU update_cache: input_pos must be Int (SymInt not yet supported)");
83+
}
84+
const int64_t input_pos = graph.get_int(input_pos_id);
85+
if (input_pos < 0) {
86+
throw std::runtime_error(
87+
"WebGPU update_cache: input_pos must be non-negative");
88+
}
89+
90+
// Bound input_pos in u64 so the u32 param downcasts cannot overflow/truncate.
91+
const uint64_t stride = n_heads * head_dim;
92+
if (cache_numel > UINT32_MAX || value_numel > cache_numel ||
93+
static_cast<uint64_t>(input_pos) > (cache_numel - value_numel) / stride) {
94+
throw std::runtime_error(
95+
"WebGPU update_cache: input_pos writes past cache capacity");
96+
}
97+
const uint64_t dst_offset = static_cast<uint64_t>(input_pos) * stride;
98+
99+
UpdateCacheParams params = {};
100+
params.numel = static_cast<uint32_t>(value_numel);
101+
params.dst_offset = static_cast<uint32_t>(dst_offset);
102+
params.cache_numel = static_cast<uint32_t>(cache_numel);
103+
104+
// Validate dispatch against device limits before allocating GPU objects.
105+
const uint32_t wg_size =
106+
utils::clamp_workgroup_size(device, kUpdateCacheWorkgroupSizeX);
107+
const uint32_t workgroup_count_x = utils::compute_1d_workgroup_count(
108+
device, params.numel, wg_size, "update_cache");
109+
110+
WGPUBufferDescriptor uniform_desc = {};
111+
uniform_desc.size = sizeof(UpdateCacheParams);
112+
uniform_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
113+
uniform_desc.mappedAtCreation = true;
114+
WGPUBuffer uniform_buffer = wgpuDeviceCreateBuffer(device, &uniform_desc);
115+
void* mapped =
116+
wgpuBufferGetMappedRange(uniform_buffer, 0, sizeof(UpdateCacheParams));
117+
std::memcpy(mapped, &params, sizeof(UpdateCacheParams));
118+
wgpuBufferUnmap(uniform_buffer);
119+
120+
graph.add_uniform_buffer_bytes(sizeof(UpdateCacheParams));
121+
122+
WGPUShaderSourceWGSL wgsl_desc = {};
123+
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
124+
wgsl_desc.code = {kUpdateCacheWGSL, WGPU_STRLEN};
125+
126+
WGPUShaderModuleDescriptor shader_desc = {};
127+
shader_desc.nextInChain = &wgsl_desc.chain;
128+
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
129+
130+
// Bind group layout: cache (rw storage) + value (ro storage) + params.
131+
WGPUBindGroupLayoutEntry entries[3] = {};
132+
entries[0].binding = 0;
133+
entries[0].visibility = WGPUShaderStage_Compute;
134+
entries[0].buffer.type = WGPUBufferBindingType_Storage;
135+
entries[1].binding = 1;
136+
entries[1].visibility = WGPUShaderStage_Compute;
137+
entries[1].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
138+
entries[2].binding = 2;
139+
entries[2].visibility = WGPUShaderStage_Compute;
140+
entries[2].buffer.type = WGPUBufferBindingType_Uniform;
141+
142+
WGPUBindGroupLayoutDescriptor bgl_desc = {};
143+
bgl_desc.entryCount = 3;
144+
bgl_desc.entries = entries;
145+
WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
146+
147+
WGPUPipelineLayoutDescriptor pl_desc = {};
148+
pl_desc.bindGroupLayoutCount = 1;
149+
pl_desc.bindGroupLayouts = &bgl;
150+
WGPUPipelineLayout pipeline_layout =
151+
wgpuDeviceCreatePipelineLayout(device, &pl_desc);
152+
153+
WGPUConstantEntry wg_size_constant = {};
154+
wg_size_constant.key = {"wg_size", WGPU_STRLEN};
155+
wg_size_constant.value = static_cast<double>(wg_size);
156+
157+
WGPUComputePipelineDescriptor pipeline_desc = {};
158+
pipeline_desc.layout = pipeline_layout;
159+
pipeline_desc.compute.module = shader;
160+
pipeline_desc.compute.entryPoint = {"main", WGPU_STRLEN};
161+
pipeline_desc.compute.constantCount = 1;
162+
pipeline_desc.compute.constants = &wg_size_constant;
163+
WGPUComputePipeline pipeline =
164+
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
165+
166+
WGPUBindGroupEntry bg_entries[3] = {};
167+
bg_entries[0].binding = 0;
168+
bg_entries[0].buffer = cache_tensor.buffer;
169+
bg_entries[0].size = cache_tensor.nbytes;
170+
bg_entries[1].binding = 1;
171+
bg_entries[1].buffer = value_tensor.buffer;
172+
bg_entries[1].size = value_tensor.nbytes;
173+
bg_entries[2].binding = 2;
174+
bg_entries[2].buffer = uniform_buffer;
175+
bg_entries[2].size = sizeof(UpdateCacheParams);
176+
177+
WGPUBindGroupDescriptor bg_desc = {};
178+
bg_desc.layout = bgl;
179+
bg_desc.entryCount = 3;
180+
bg_desc.entries = bg_entries;
181+
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
182+
183+
graph.add_dispatch({pipeline, bind_group, workgroup_count_x});
184+
185+
wgpuShaderModuleRelease(shader);
186+
wgpuBindGroupLayoutRelease(bgl);
187+
wgpuPipelineLayoutRelease(pipeline_layout);
188+
// Drop our ref; the bind group keeps the uniform buffer alive until release.
189+
wgpuBufferRelease(uniform_buffer);
190+
}
191+
192+
} // namespace
193+
194+
WEBGPU_REGISTER_OPERATORS {
195+
WEBGPU_REGISTER_OP(update_cache.default, update_cache_impl);
196+
}
197+
198+
} // namespace executorch::backends::webgpu
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@group(0) @binding(0) var<storage, read_write> t_cache: array<f32>;
2+
@group(0) @binding(1) var<storage, read> t_value: array<f32>;
3+
4+
struct Params {
5+
numel: u32,
6+
dst_offset: u32,
7+
cache_numel: u32,
8+
_pad0: u32,
9+
}
10+
@group(0) @binding(2) var<uniform> params: Params;
11+
12+
override wg_size: u32 = 256;
13+
14+
@compute @workgroup_size(wg_size, 1, 1)
15+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
16+
let i = gid.x;
17+
if (i >= params.numel) {
18+
return;
19+
}
20+
if (params.dst_offset + i >= params.cache_numel) {
21+
return;
22+
}
23+
t_cache[params.dst_offset + i] = t_value[i];
24+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 <cstdint>
12+
13+
namespace executorch::backends::webgpu {
14+
15+
// @generated from update_cache.wgsl - DO NOT EDIT.
16+
// wgsl-sha256: 994cac9bab0ed25c9c82d54af77d9bbbe34e49419e916d0164c9cf0e5b199c6a
17+
inline constexpr const char* kUpdateCacheWGSL = R"(
18+
@group(0) @binding(0) var<storage, read_write> t_cache: array<f32>;
19+
@group(0) @binding(1) var<storage, read> t_value: array<f32>;
20+
21+
struct Params {
22+
numel: u32,
23+
dst_offset: u32,
24+
cache_numel: u32,
25+
_pad0: u32,
26+
}
27+
@group(0) @binding(2) var<uniform> params: Params;
28+
29+
override wg_size: u32 = 256;
30+
31+
@compute @workgroup_size(wg_size, 1, 1)
32+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
33+
let i = gid.x;
34+
if (i >= params.numel) {
35+
return;
36+
}
37+
if (params.dst_offset + i >= params.cache_numel) {
38+
return;
39+
}
40+
t_cache[params.dst_offset + i] = t_value[i];
41+
}
42+
)";
43+
44+
inline constexpr uint32_t kUpdateCacheWorkgroupSizeX = 256;
45+
inline constexpr uint32_t kUpdateCacheWorkgroupSizeY = 1;
46+
inline constexpr uint32_t kUpdateCacheWorkgroupSizeZ = 1;
47+
48+
} // namespace executorch::backends::webgpu

0 commit comments

Comments
 (0)