Skip to content

Commit ef77d1d

Browse files
JCNTHfacebook-github-bot
authored andcommitted
Add rms_norm op
Summary: Adds the `et_vk.rms_norm.default` operator to the WebGPU backend: a WGSL compute shader using a cooperative tree reduction, one workgroup per row. The shader mirrors the Vulkan implementation (`backends/vulkan/runtime/graph/ops/impl/RmsNorm.cpp`, `backends/vulkan/runtime/graph/ops/glsl/rms_norm_buffer.glsl`); indexing assumes contiguous fp32 inputs. The handler fails loud (throws, mirroring Vulkan's `VK_CHECK_COND`) on invalid shape/dtype/dispatch-limit conditions, and defaults `eps` to the float32 machine epsilon. The weight constant is uploaded via the named-data path added in the parent diff. Differential Revision: D106887028
1 parent c684b12 commit ef77d1d

9 files changed

Lines changed: 781 additions & 7 deletions

File tree

backends/webgpu/CMakeLists.txt

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ if(NOT TARGET vulkan_schema)
2626
endif()
2727

2828
set(WEBGPU_SRCS
29-
runtime/WebGPUBackend.cpp runtime/WebGPUGraph.cpp
30-
runtime/WebGPUDelegateHeader.cpp runtime/WebGPUDevice.cpp
31-
runtime/ops/OperatorRegistry.cpp runtime/ops/add/BinaryOp.cpp
29+
runtime/WebGPUBackend.cpp
30+
runtime/WebGPUGraph.cpp
31+
runtime/WebGPUDelegateHeader.cpp
32+
runtime/WebGPUDevice.cpp
33+
runtime/ops/OperatorRegistry.cpp
34+
runtime/ops/add/BinaryOp.cpp
35+
runtime/ops/rms_norm/RmsNorm.cpp
3236
)
3337

3438
add_library(webgpu_backend ${WEBGPU_SRCS})
@@ -116,4 +120,35 @@ if(EXECUTORCH_BUILD_WEBGPU_TEST)
116120

117121
target_compile_options(webgpu_native_test PRIVATE -fexceptions)
118122
set_property(TARGET webgpu_native_test PROPERTY CXX_STANDARD 17)
123+
124+
add_executable(webgpu_rms_norm_test test/native/test_rms_norm.cpp)
125+
126+
target_include_directories(
127+
webgpu_rms_norm_test PRIVATE $<BUILD_INTERFACE:${EXECUTORCH_ROOT}/..>
128+
"${WGPU_NATIVE_DIR}/include"
129+
)
130+
131+
target_link_libraries(
132+
webgpu_rms_norm_test
133+
PRIVATE webgpu_backend
134+
wgpu_native
135+
executorch_core
136+
extension_module_static
137+
extension_data_loader
138+
extension_tensor
139+
portable_kernels
140+
portable_ops_lib
141+
)
142+
143+
if(APPLE)
144+
target_link_libraries(
145+
webgpu_rms_norm_test PRIVATE "-framework Metal" "-framework QuartzCore"
146+
"-framework CoreGraphics"
147+
)
148+
else()
149+
target_link_libraries(webgpu_rms_norm_test PRIVATE dl m pthread)
150+
endif()
151+
152+
target_compile_options(webgpu_rms_norm_test PRIVATE -fexceptions)
153+
set_property(TARGET webgpu_rms_norm_test PROPERTY CXX_STANDARD 17)
119154
endif()
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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/ops/OperatorRegistry.h>
11+
#include <executorch/backends/webgpu/runtime/ops/rms_norm/rms_norm_wgsl.h>
12+
13+
#include <webgpu/webgpu.h>
14+
15+
#include <cstdint>
16+
#include <cstring>
17+
#include <stdexcept>
18+
19+
namespace executorch::backends::webgpu {
20+
21+
namespace {
22+
23+
// Uniform layout matching the WGSL Params struct (16-byte aligned).
24+
struct RmsNormParams {
25+
uint32_t num_rows;
26+
uint32_t row_width;
27+
float epsilon;
28+
uint32_t _pad;
29+
};
30+
static_assert(sizeof(RmsNormParams) == 16, "RmsNormParams must be 16 bytes");
31+
32+
void rms_norm_impl(WebGPUGraph& graph, const std::vector<int>& args) {
33+
// et_vk.rms_norm.default args: [in, weight, eps, out]
34+
const int in_id = args.at(0);
35+
const int weight_id = args.at(1);
36+
const int eps_id = args.at(2);
37+
const int out_id = args.at(3);
38+
39+
WGPUDevice device = graph.device();
40+
41+
// Get epsilon (Double from a Python float; defaults to float32 eps)
42+
float epsilon = 1.1920928955078125e-07f;
43+
if (graph.get_value_type(eps_id) == WebGPUGraph::ValueType::Double) {
44+
epsilon = static_cast<float>(graph.get_double(eps_id));
45+
} else if (graph.get_value_type(eps_id) == WebGPUGraph::ValueType::Int) {
46+
epsilon = static_cast<float>(graph.get_int(eps_id));
47+
}
48+
49+
// row_width = last dim; num_rows = product of the rest (PyTorch NCHW order)
50+
const auto& in_tensor = graph.get_tensor(in_id);
51+
if (in_tensor.dims.empty() || in_tensor.nbytes == 0) {
52+
throw std::runtime_error("WebGPU rms_norm: empty input");
53+
}
54+
const uint32_t row_width = static_cast<uint32_t>(in_tensor.dims.back());
55+
if (row_width == 0) {
56+
throw std::runtime_error("WebGPU rms_norm: zero row width");
57+
}
58+
uint64_t in_numel = 1;
59+
for (int64_t d : in_tensor.dims) {
60+
in_numel *= static_cast<uint64_t>(d);
61+
}
62+
// fp32-only shader: bail if the bytes don't match an fp32 element count.
63+
if (in_tensor.nbytes != in_numel * sizeof(float)) {
64+
throw std::runtime_error("WebGPU rms_norm: fp32-only (byte-size mismatch)");
65+
}
66+
const uint32_t num_rows = static_cast<uint32_t>(in_numel / row_width);
67+
if (num_rows == 0) {
68+
throw std::runtime_error("WebGPU rms_norm: zero rows");
69+
}
70+
71+
// Create uniform buffer for params
72+
RmsNormParams params = {};
73+
params.num_rows = num_rows;
74+
params.row_width = row_width;
75+
params.epsilon = epsilon;
76+
77+
WGPUBufferDescriptor uniform_desc = {};
78+
uniform_desc.size = sizeof(RmsNormParams);
79+
uniform_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
80+
uniform_desc.mappedAtCreation = true;
81+
WGPUBuffer uniform_buffer = wgpuDeviceCreateBuffer(device, &uniform_desc);
82+
void* mapped =
83+
wgpuBufferGetMappedRange(uniform_buffer, 0, sizeof(RmsNormParams));
84+
std::memcpy(mapped, &params, sizeof(RmsNormParams));
85+
wgpuBufferUnmap(uniform_buffer);
86+
87+
graph.add_uniform_buffer_bytes(sizeof(RmsNormParams));
88+
89+
// Create shader module from built-in WGSL source
90+
WGPUShaderSourceWGSL wgsl_desc = {};
91+
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
92+
wgsl_desc.code = {kRmsNormWGSL, WGPU_STRLEN};
93+
94+
WGPUShaderModuleDescriptor shader_desc = {};
95+
shader_desc.nextInChain = &wgsl_desc.chain;
96+
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
97+
98+
// Create bind group layout: out (rw) + in/weight (ro storage) + params
99+
WGPUBindGroupLayoutEntry entries[4] = {};
100+
101+
// t_out - storage buffer, read-write
102+
entries[0].binding = 0;
103+
entries[0].visibility = WGPUShaderStage_Compute;
104+
entries[0].buffer.type = WGPUBufferBindingType_Storage;
105+
106+
// t_in - storage buffer, read-only
107+
entries[1].binding = 1;
108+
entries[1].visibility = WGPUShaderStage_Compute;
109+
entries[1].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
110+
111+
// t_weight - storage buffer, read-only
112+
entries[2].binding = 2;
113+
entries[2].visibility = WGPUShaderStage_Compute;
114+
entries[2].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
115+
116+
// params - uniform buffer
117+
entries[3].binding = 3;
118+
entries[3].visibility = WGPUShaderStage_Compute;
119+
entries[3].buffer.type = WGPUBufferBindingType_Uniform;
120+
121+
WGPUBindGroupLayoutDescriptor bgl_desc = {};
122+
bgl_desc.entryCount = 4;
123+
bgl_desc.entries = entries;
124+
WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
125+
126+
// Create pipeline layout
127+
WGPUPipelineLayoutDescriptor pl_desc = {};
128+
pl_desc.bindGroupLayoutCount = 1;
129+
pl_desc.bindGroupLayouts = &bgl;
130+
WGPUPipelineLayout pipeline_layout =
131+
wgpuDeviceCreatePipelineLayout(device, &pl_desc);
132+
133+
// Create compute pipeline
134+
WGPUComputePipelineDescriptor pipeline_desc = {};
135+
pipeline_desc.layout = pipeline_layout;
136+
pipeline_desc.compute.module = shader;
137+
pipeline_desc.compute.entryPoint = {"main", WGPU_STRLEN};
138+
WGPUComputePipeline pipeline =
139+
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
140+
141+
// Create bind group with actual buffers
142+
const auto& out_tensor = graph.get_tensor(out_id);
143+
const auto& weight_tensor = graph.get_tensor(weight_id);
144+
145+
WGPUBindGroupEntry bg_entries[4] = {};
146+
147+
bg_entries[0].binding = 0;
148+
bg_entries[0].buffer = out_tensor.buffer;
149+
bg_entries[0].size = out_tensor.nbytes;
150+
151+
bg_entries[1].binding = 1;
152+
bg_entries[1].buffer = in_tensor.buffer;
153+
bg_entries[1].size = in_tensor.nbytes;
154+
155+
bg_entries[2].binding = 2;
156+
bg_entries[2].buffer = weight_tensor.buffer;
157+
bg_entries[2].size = weight_tensor.nbytes;
158+
159+
bg_entries[3].binding = 3;
160+
bg_entries[3].buffer = uniform_buffer;
161+
bg_entries[3].size = sizeof(RmsNormParams);
162+
163+
WGPUBindGroupDescriptor bg_desc = {};
164+
bg_desc.layout = bgl;
165+
bg_desc.entryCount = 4;
166+
bg_desc.entries = bg_entries;
167+
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
168+
169+
// One workgroup per row (kRmsNormWorkgroupSize threads cooperate per row)
170+
static_assert(
171+
kRmsNormWorkgroupSize == 64,
172+
"must match @workgroup_size and WG_SIZE in rms_norm.wgsl");
173+
if (num_rows > 65535u) {
174+
throw std::runtime_error(
175+
"WebGPU rms_norm: num_rows exceeds the 1D dispatch limit (65535)");
176+
}
177+
graph.add_dispatch({pipeline, bind_group, num_rows});
178+
179+
// Release intermediate objects (pipeline and bind_group are kept by dispatch)
180+
wgpuShaderModuleRelease(shader);
181+
wgpuBindGroupLayoutRelease(bgl);
182+
wgpuPipelineLayoutRelease(pipeline_layout);
183+
// uniform_buffer is kept alive by the bind group
184+
}
185+
186+
} // namespace
187+
188+
WEBGPU_REGISTER_OPERATORS {
189+
WEBGPU_REGISTER_OP(et_vk.rms_norm.default, rms_norm_impl);
190+
}
191+
192+
} // namespace executorch::backends::webgpu
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
@group(0) @binding(0) var<storage, read_write> t_out: array<f32>;
2+
@group(0) @binding(1) var<storage, read> t_in: array<f32>;
3+
@group(0) @binding(2) var<storage, read> t_weight: array<f32>;
4+
5+
struct Params {
6+
num_rows: u32,
7+
row_width: u32,
8+
epsilon: f32,
9+
_pad: u32,
10+
}
11+
@group(0) @binding(3) var<uniform> params: Params;
12+
13+
const WG_SIZE: u32 = 64u;
14+
15+
var<workgroup> shared_sum: array<f32, WG_SIZE>;
16+
17+
fn reduce_shared(worker_id: u32) {
18+
workgroupBarrier();
19+
var stride: u32 = WG_SIZE / 2u;
20+
loop {
21+
if (stride == 0u) {
22+
break;
23+
}
24+
if (worker_id < stride) {
25+
shared_sum[worker_id] = shared_sum[worker_id] + shared_sum[worker_id + stride];
26+
}
27+
workgroupBarrier();
28+
stride = stride >> 1u;
29+
}
30+
}
31+
32+
@compute @workgroup_size(64, 1, 1)
33+
fn main(
34+
@builtin(workgroup_id) wid: vec3<u32>,
35+
@builtin(local_invocation_id) lid: vec3<u32>) {
36+
let row_idx = wid.x;
37+
let worker_id = lid.x;
38+
39+
if (row_idx >= params.num_rows) {
40+
return;
41+
}
42+
43+
let base = row_idx * params.row_width;
44+
45+
var local_sq_sum: f32 = 0.0;
46+
var x: u32 = worker_id;
47+
loop {
48+
if (x >= params.row_width) {
49+
break;
50+
}
51+
let v = t_in[base + x];
52+
local_sq_sum = local_sq_sum + v * v;
53+
x = x + WG_SIZE;
54+
}
55+
56+
shared_sum[worker_id] = local_sq_sum;
57+
reduce_shared(worker_id);
58+
59+
let mean_sq = shared_sum[0] / f32(params.row_width);
60+
let rstd = inverseSqrt(mean_sq + params.epsilon);
61+
62+
x = worker_id;
63+
loop {
64+
if (x >= params.row_width) {
65+
break;
66+
}
67+
let v = t_in[base + x];
68+
let w = t_weight[x];
69+
t_out[base + x] = v * rstd * w;
70+
x = x + WG_SIZE;
71+
}
72+
}

0 commit comments

Comments
 (0)