Skip to content

Commit a20d90a

Browse files
committed
[ExecuTorch][WebGPU] Add HuggingFace rotate-half RoPE
Pull Request resolved: #21135 **Add HuggingFace rotate-half RoPE and migrate RoPE to shared dispatch construction** Qwen models pair the first and second halves of each head vector rather than adjacent elements. This adds the rotate-half operator with dynamic start-position updates, full-dimension Q/K handling, generated WGSL, and strict frequency-table bounds. The shared RoPE handler also closes the dispatch-boilerplate review: it uses `graph.device()`, typed graph-owned parameter buffers, descriptor-driven bindings and pipelines, named validation and resize callbacks, generated shader-registry lookup, scoped ownership, and graph-owned workgroup recomputation. Interleaved shader math and output are unchanged. Key changes: - `rotary_embedding_hf.wgsl` and generated registry entry — one thread per pair for HuggingFace rotate-half. - `RotaryEmbedding.cpp` — named validation, typed resize contexts, shared dispatch descriptors, and one initial/resize grid picker per route. - Native tests — malformed input rejection, dynamic bounds, shrink/regrow reuse, and HF/interleaved lifecycle coverage. Co-authored-with: Claude Code. ghstack-source-id: 411961455 @exported-using-ghexport Differential Revision: [D113171746](https://our.internmc.facebook.com/intern/diff/D113171746/)
1 parent 012642a commit a20d90a

10 files changed

Lines changed: 1388 additions & 564 deletions

backends/webgpu/runtime/ops/rope/RotaryEmbedding.cpp

Lines changed: 423 additions & 530 deletions
Large diffs are not rendered by default.

backends/webgpu/runtime/ops/rope/rotary_embedding_hf.wgsl

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ struct Params {
1818
override wg_size: u32 = 64u;
1919

2020
// One thread per (i, i+half_dim) pair; HuggingFace rotate-half RoPE, shared
21-
// xq/xk shader. freqs is the FULL [max_seq, rotary_dim] table (duplicated
22-
// halves) indexed at row (start_pos + s); only the first-half column is read.
21+
// xq/xk shader. freqs is the FULL [max_seq, rotary_dim] table indexed at row
22+
// (start_pos + s); each output half uses its corresponding frequency column.
2323
@compute @workgroup_size(wg_size, 1, 1)
24-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
25-
let pair = gid.x;
24+
fn main(
25+
@builtin(global_invocation_id) gid: vec3<u32>,
26+
@builtin(num_workgroups) num_workgroups: vec3<u32>) {
27+
let pair = gid.x + gid.y * (num_workgroups.x * wg_size);
2628
if (pair >= params.num_pairs) {
2729
return;
2830
}
@@ -38,12 +40,16 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
3840
((b * params.seq + s) * params.n_heads + head) * params.head_dim;
3941
let a_idx = head_base + pair_i;
4042
let b_idx = head_base + pair_i + half_dim;
41-
let freqs_idx = (s + params.start_pos) * params.rotary_dim + pair_i;
43+
let freqs_base = (s + params.start_pos) * params.rotary_dim;
44+
let freqs_a_idx = freqs_base + pair_i;
45+
let freqs_b_idx = freqs_a_idx + half_dim;
4246

43-
let c = t_freqs_cos[freqs_idx];
44-
let si = t_freqs_sin[freqs_idx];
47+
let c_a = t_freqs_cos[freqs_a_idx];
48+
let si_a = t_freqs_sin[freqs_a_idx];
49+
let c_b = t_freqs_cos[freqs_b_idx];
50+
let si_b = t_freqs_sin[freqs_b_idx];
4551
let x_a = t_in[a_idx];
4652
let x_b = t_in[b_idx];
47-
t_out[a_idx] = x_a * c - x_b * si;
48-
t_out[b_idx] = x_b * c + x_a * si;
53+
t_out[a_idx] = x_a * c_a - x_b * si_a;
54+
t_out[b_idx] = x_b * c_b + x_a * si_b;
4955
}

backends/webgpu/runtime/ops/rope/rotary_embedding_hf_wgsl.h

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace executorch::backends::webgpu {
1414

1515
// @generated from rotary_embedding_hf.wgsl - DO NOT EDIT.
16-
// wgsl-sha256: 5ba8d45925f00f12af17bf3092a1af9513a9e501c5c35e6b0d48cfb3dac7b5d6
16+
// wgsl-sha256: 4f081ed4c8165f021cbb722d379e437f30b8dfb08bf03bfcbaa406ed7799c7b6
1717
inline constexpr const char* kRotaryEmbeddingHfWGSL = R"(
1818
@group(0) @binding(0) var<storage, read_write> t_out: array<f32>;
1919
@group(0) @binding(1) var<storage, read> t_in: array<f32>;
@@ -35,11 +35,13 @@ struct Params {
3535
override wg_size: u32 = 64u;
3636
3737
// One thread per (i, i+half_dim) pair; HuggingFace rotate-half RoPE, shared
38-
// xq/xk shader. freqs is the FULL [max_seq, rotary_dim] table (duplicated
39-
// halves) indexed at row (start_pos + s); only the first-half column is read.
38+
// xq/xk shader. freqs is the FULL [max_seq, rotary_dim] table indexed at row
39+
// (start_pos + s); each output half uses its corresponding frequency column.
4040
@compute @workgroup_size(wg_size, 1, 1)
41-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
42-
let pair = gid.x;
41+
fn main(
42+
@builtin(global_invocation_id) gid: vec3<u32>,
43+
@builtin(num_workgroups) num_workgroups: vec3<u32>) {
44+
let pair = gid.x + gid.y * (num_workgroups.x * wg_size);
4345
if (pair >= params.num_pairs) {
4446
return;
4547
}
@@ -55,14 +57,18 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
5557
((b * params.seq + s) * params.n_heads + head) * params.head_dim;
5658
let a_idx = head_base + pair_i;
5759
let b_idx = head_base + pair_i + half_dim;
58-
let freqs_idx = (s + params.start_pos) * params.rotary_dim + pair_i;
60+
let freqs_base = (s + params.start_pos) * params.rotary_dim;
61+
let freqs_a_idx = freqs_base + pair_i;
62+
let freqs_b_idx = freqs_a_idx + half_dim;
5963
60-
let c = t_freqs_cos[freqs_idx];
61-
let si = t_freqs_sin[freqs_idx];
64+
let c_a = t_freqs_cos[freqs_a_idx];
65+
let si_a = t_freqs_sin[freqs_a_idx];
66+
let c_b = t_freqs_cos[freqs_b_idx];
67+
let si_b = t_freqs_sin[freqs_b_idx];
6268
let x_a = t_in[a_idx];
6369
let x_b = t_in[b_idx];
64-
t_out[a_idx] = x_a * c - x_b * si;
65-
t_out[b_idx] = x_b * c + x_a * si;
70+
t_out[a_idx] = x_a * c_a - x_b * si_a;
71+
t_out[b_idx] = x_b * c_b + x_a * si_b;
6672
}
6773
)";
6874

backends/webgpu/scripts/test_webgpu_native_ci.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ DISPATCH_ORDER_DIR="/tmp/dispatch_order"
6565
UPDATE_CACHE_DIR="/tmp/update_cache"
6666
INDEX_DIR="/tmp/index"
6767
DYNAMIC_SHAPE_DIR="/tmp/dynamic_shape"
68+
ROPE_HF_DIR="/tmp/webgpu_rope_hf"
6869
SYMINT_BLOB="/tmp/sdpa_dyn_small.pte"
6970
OUTPUT_SUPPRESSION_DIR="/tmp/output_suppression"
7071
EMBEDDING_MODEL="/tmp/webgpu_embedding_q4gsw.pte"
@@ -104,6 +105,11 @@ export_rope_model('${ROPE_MODEL}', '${ROPE_XQ_GOLDEN}', '${ROPE_XK_GOLDEN}')
104105
export_rope_model('${ROPE_DECODE_MODEL}', '${ROPE_DECODE_XQ_GOLDEN}', '${ROPE_DECODE_XK_GOLDEN}', 'decode')
105106
"
106107

108+
$PYTHON_EXECUTABLE -c "
109+
from executorch.backends.webgpu.test.ops.test_rope_hf import export_rope_hf_dynamic
110+
export_rope_hf_dynamic('${ROPE_HF_DIR}')
111+
"
112+
107113
$PYTHON_EXECUTABLE -c "
108114
from executorch.backends.webgpu.test.ops.test_prepack import export_prepack_model, export_prepack_two_const_model, export_prepack_tied_const_model
109115
export_prepack_model('${PREPACK_MODEL}', '${PREPACK_GOLDEN}')
@@ -150,6 +156,7 @@ export_dynamic_decode('/tmp')
150156
export_incache_decode('/tmp')
151157
"
152158

159+
require_file "${ROPE_HF_DIR}/rope_hf_dynamic.pte"
153160
require_file "${SYMINT_BLOB}"
154161
require_file "${OUTPUT_SUPPRESSION_DIR}/input.bin"
155162

@@ -201,6 +208,7 @@ run_with_required_device env WEBGPU_TEST_SDPA_DIR=/tmp/ \
201208
WEBGPU_TEST_ROPE_DECODE_MODEL="${ROPE_DECODE_MODEL}" \
202209
WEBGPU_TEST_ROPE_DECODE_XQ_GOLDEN="${ROPE_DECODE_XQ_GOLDEN}" \
203210
WEBGPU_TEST_ROPE_DECODE_XK_GOLDEN="${ROPE_DECODE_XK_GOLDEN}" \
211+
WEBGPU_TEST_ROPE_HF_DIR="${ROPE_HF_DIR}" \
204212
WEBGPU_TEST_SYMINT_BLOB="${SYMINT_BLOB}" \
205213
WEBGPU_TEST_PREPACK_MODEL="${PREPACK_MODEL}" \
206214
WEBGPU_TEST_PREPACK_GOLDEN="${PREPACK_GOLDEN}" \

backends/webgpu/test/native/test_compute_dispatch.cpp

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <executorch/backends/webgpu/runtime/WebGPUGraph.h>
1212
#include <executorch/backends/webgpu/runtime/WebGPUShaderRegistry.h>
1313
#include <executorch/backends/webgpu/runtime/WebGPUUtils.h>
14+
#include <executorch/backends/webgpu/runtime/ops/OperatorRegistry.h>
1415
#include <executorch/backends/webgpu/runtime/ops/relu/relu_wgsl.h>
1516
#include <executorch/backends/webgpu/runtime/ops/sigmoid/sigmoid_wgsl.h>
1617

@@ -20,6 +21,8 @@
2021
#include <cstdio>
2122
#include <limits>
2223
#include <stdexcept>
24+
#include <string>
25+
#include <vector>
2326

2427
namespace executorch::backends::webgpu {
2528
namespace {
@@ -653,6 +656,140 @@ TEST(WebGPUDynamicDispatch, RejectsRouteOverlapWithoutPoisoningRegistry) {
653656
expect_dispatch_grid(graph, 2u, 13u, 17u);
654657
}
655658

659+
struct InvalidRopeGraphCase {
660+
const char* name;
661+
std::vector<uint32_t> xq_dims;
662+
std::vector<uint32_t> xk_dims;
663+
std::vector<uint32_t> cos_dims;
664+
std::vector<uint32_t> sin_dims;
665+
std::vector<uint32_t> xq_out_dims;
666+
std::vector<uint32_t> xk_out_dims;
667+
vkgraph::VkDataType xq_dtype;
668+
const char* expected_error;
669+
};
670+
671+
void expect_invalid_rope_graph(const InvalidRopeGraphCase& test_case) {
672+
namespace vk = vkgraph;
673+
::flatbuffers::FlatBufferBuilder fbb;
674+
std::vector<::flatbuffers::Offset<vk::VkValue>> values;
675+
auto add_tensor = [&](vk::VkDataType dtype,
676+
const std::vector<uint32_t>& dims,
677+
int mem_obj_id) {
678+
values.push_back(vk::CreateVkValue(
679+
fbb,
680+
vk::GraphTypes::VkTensor,
681+
vk::CreateVkTensorDirect(
682+
fbb, dtype, &dims, /*constant_id=*/-1, mem_obj_id)
683+
.Union()));
684+
};
685+
add_tensor(test_case.xq_dtype, test_case.xq_dims, 0);
686+
add_tensor(vk::VkDataType::FLOAT32, test_case.xk_dims, 1);
687+
add_tensor(vk::VkDataType::FLOAT32, test_case.cos_dims, 2);
688+
add_tensor(vk::VkDataType::FLOAT32, test_case.sin_dims, 3);
689+
add_tensor(vk::VkDataType::FLOAT32, test_case.xq_out_dims, 4);
690+
add_tensor(vk::VkDataType::FLOAT32, test_case.xk_out_dims, 5);
691+
std::vector<int32_t> output_value_ids = {4, 5};
692+
values.push_back(vk::CreateVkValue(
693+
fbb,
694+
vk::GraphTypes::ValueList,
695+
vk::CreateValueListDirect(fbb, &output_value_ids).Union()));
696+
697+
std::vector<int32_t> args = {0, 1, 2, 3, 6};
698+
std::vector<::flatbuffers::Offset<vk::OperatorCall>> chain;
699+
chain.push_back(vk::CreateOperatorCallDirect(
700+
fbb, 0, "et_vk.apply_rotary_emb.default", &args));
701+
std::vector<uint32_t> input_ids = {0, 1, 2, 3};
702+
std::vector<uint32_t> output_ids = {4, 5};
703+
const auto root = vk::CreateVkGraphDirect(
704+
fbb, "0", &chain, &values, &input_ids, &output_ids);
705+
vk::FinishVkGraphBuffer(fbb, root);
706+
707+
WebGPUGraph graph;
708+
std::string error;
709+
try {
710+
graph.build(fbb.GetBufferPointer(), nullptr, nullptr);
711+
} catch (const std::exception& exception) {
712+
error = exception.what();
713+
}
714+
EXPECT_FALSE(error.empty()) << test_case.name << " unexpectedly built";
715+
EXPECT_EQ(error, test_case.expected_error)
716+
<< test_case.name << " rejected for the wrong reason";
717+
const WebGPUMemoryStats stats = graph.memory_stats();
718+
EXPECT_EQ(stats.num_dispatches, 0) << test_case.name;
719+
EXPECT_EQ(stats.uniform_buffer_bytes, 0u) << test_case.name;
720+
EXPECT_EQ(stats.num_cached_shaders, 0) << test_case.name;
721+
EXPECT_EQ(stats.num_cached_pipelines, 0) << test_case.name;
722+
}
723+
724+
TEST(WebGPURopeValidation, RejectsMalformedGraphsBeforeDispatchAllocation) {
725+
ASSERT_TRUE(
726+
webgpu_operator_registry().has_op("et_vk.apply_rotary_emb.default"));
727+
const std::vector<uint32_t> xq = {1, 2, 2, 4};
728+
const std::vector<uint32_t> xk = {1, 2, 1, 4};
729+
const std::vector<uint32_t> freqs = {2, 2};
730+
const InvalidRopeGraphCase cases[] = {
731+
{"query rank",
732+
{2, 4},
733+
xk,
734+
freqs,
735+
freqs,
736+
{2, 4},
737+
xk,
738+
vkgraph::VkDataType::FLOAT32,
739+
"WebGPU apply_rotary_emb: malformed dims"},
740+
{"sequence mismatch",
741+
xq,
742+
{1, 3, 1, 4},
743+
freqs,
744+
freqs,
745+
xq,
746+
{1, 3, 1, 4},
747+
vkgraph::VkDataType::FLOAT32,
748+
"WebGPU apply_rotary_emb: xq/xk head_dim and seq must match"},
749+
{"head dimension mismatch",
750+
xq,
751+
{1, 2, 1, 6},
752+
freqs,
753+
freqs,
754+
xq,
755+
{1, 2, 1, 6},
756+
vkgraph::VkDataType::FLOAT32,
757+
"WebGPU apply_rotary_emb: xq/xk head_dim and seq must match"},
758+
{"frequency width mismatch",
759+
xq,
760+
xk,
761+
{2, 3},
762+
{2, 3},
763+
xq,
764+
xk,
765+
vkgraph::VkDataType::FLOAT32,
766+
"WebGPU apply_rotary_emb: head_dim != 2 * freqs_cos last dim"},
767+
{"cosine/sine shape mismatch",
768+
xq,
769+
xk,
770+
freqs,
771+
{2, 1},
772+
xq,
773+
xk,
774+
vkgraph::VkDataType::FLOAT32,
775+
"WebGPU apply_rotary_emb: freqs_cos and freqs_sin shapes differ"},
776+
{"query byte size mismatch",
777+
xq,
778+
xk,
779+
freqs,
780+
freqs,
781+
xq,
782+
xk,
783+
vkgraph::VkDataType::INT64,
784+
"WebGPU apply_rotary_emb: dtype/byte-size mismatch (all fp32) or "
785+
"freqs shape != [seq, head_dim/2]"},
786+
};
787+
for (const InvalidRopeGraphCase& test_case : cases) {
788+
SCOPED_TRACE(test_case.name);
789+
expect_invalid_rope_graph(test_case);
790+
}
791+
}
792+
656793
TEST(WebGPUExecution, FullySuppressedPlanPerformsNoQueueSubmission) {
657794
WebGPUGraph graph;
658795
const WebGPUExecutionPlan plan;

0 commit comments

Comments
 (0)