Skip to content

Commit 430b73d

Browse files
authored
Reland two-pass channelwise gated delta rule kernel + OSS-safe bench BUCK (#21105)
Differential Revision: D113076546 Pull Request resolved: #21105
1 parent 3802831 commit 430b73d

3 files changed

Lines changed: 131 additions & 27 deletions

File tree

extension/llm/custom_ops/BUCK

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,20 @@ fbcode_target(_kind = runtime.python_test,
9191
"//caffe2:torch",
9292
],
9393
)
94+
95+
fbcode_target(_kind = runtime.python_library,
96+
name = "bench_channelwise_gated_delta_rule_lib",
97+
srcs = ["bench_channelwise_gated_delta_rule.py"],
98+
base_module = "executorch.extension.llm.custom_ops",
99+
deps = ["//caffe2:torch"],
100+
)
101+
102+
fbcode_target(_kind = runtime.python_binary,
103+
name = "bench_channelwise_gated_delta_rule",
104+
main_module = "executorch.extension.llm.custom_ops.bench_channelwise_gated_delta_rule",
105+
preload_deps = [
106+
":custom_ops_aot_lib_mkl_noomp",
107+
":custom_ops_aot_py",
108+
],
109+
deps = [":bench_channelwise_gated_delta_rule_lib"],
110+
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# pyre-unsafe
8+
9+
"""Microbenchmark for the ``channelwise_gated_delta_rule`` custom op.
10+
11+
Times the fused recurrent kernel in isolation (no model) at representative
12+
GatedDeltaNet sizes, for both the decode (T=1) and prefill (T>1) regimes. Use it
13+
to compare kernel variants (e.g. naive vs. fused) — rebuild the custom op, run
14+
this, and diff the numbers.
15+
16+
Run (in an env where the custom op is built):
17+
python -m executorch.extension.llm.custom_ops.bench_channelwise_gated_delta_rule
18+
"""
19+
20+
import time
21+
22+
import torch
23+
24+
from executorch.extension.llm.custom_ops import custom_ops # noqa: F401
25+
26+
_OP = torch.ops.llama.channelwise_gated_delta_rule.default
27+
28+
29+
def _make_inputs(b: int, h: int, t: int, k: int, v: int):
30+
return (
31+
torch.randn(b, h, t, k),
32+
torch.randn(b, h, t, k),
33+
torch.randn(b, h, t, v),
34+
torch.rand(b, h, t, k),
35+
torch.rand(b, h, t),
36+
torch.randn(b, h, k, v),
37+
)
38+
39+
40+
def _bench_one(
41+
b: int, h: int, t: int, k: int, v: int, iters: int, warmup: int
42+
) -> tuple[float, float]:
43+
q, key, val, decay, beta, state = _make_inputs(b, h, t, k, v)
44+
for _ in range(warmup):
45+
_OP(q, key, val, decay, beta, state)
46+
start = time.perf_counter()
47+
for _ in range(iters):
48+
_OP(q, key, val, decay, beta, state)
49+
elapsed = time.perf_counter() - start
50+
ms_per_call = elapsed / iters * 1e3
51+
us_per_token = ms_per_call * 1e3 / t
52+
return ms_per_call, us_per_token
53+
54+
55+
def main() -> None:
56+
torch.manual_seed(0)
57+
torch.set_num_threads(1) # single-thread: the kernel has no internal threading
58+
59+
k = v = 128 # head dim (KDA / Qwen3-Next GatedDeltaNet)
60+
# (label, batch, heads, seq_len, iters)
61+
configs = [
62+
("decode T=1", 1, 32, 1, 2000),
63+
("prefill T=128", 1, 32, 128, 200),
64+
("prefill T=512", 1, 32, 512, 50),
65+
]
66+
67+
print(
68+
f"channelwise_gated_delta_rule microbenchmark "
69+
f"(K=V={k}, fp32, 1 thread, state={32 * k * v * 4 // 1024}KB)"
70+
)
71+
print(f"{'config':<15}{'B':>3}{'H':>4}{'T':>6}{'ms/call':>12}{'us/token':>12}")
72+
for label, b, h, t, iters in configs:
73+
ms, us = _bench_one(b, h, t, k, v, iters=iters, warmup=max(10, iters // 10))
74+
print(f"{label:<15}{b:>3}{h:>4}{t:>6}{ms:>12.4f}{us:>12.3f}")
75+
76+
77+
if __name__ == "__main__":
78+
main()

extension/llm/custom_ops/op_sdpa.cpp

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -773,8 +773,20 @@ std::tuple<Tensor&, Tensor&> channelwise_gated_delta_rule_out(
773773
const auto* initial_state_data = initial_state.const_data_ptr<float>();
774774
auto* state_data = final_state_out.mutable_data_ptr<float>();
775775
auto* output_data = out.mutable_data_ptr<float>();
776-
std::vector<float> v_pred(v_head_dim);
777-
std::vector<float> delta(v_head_dim);
776+
777+
const int64_t scratch_numel = 2 * v_head_dim;
778+
std::unique_ptr<float[]> fallback_scratch;
779+
float* scratch_data = nullptr;
780+
Result<void*> scratch = ctx.allocate_temp(
781+
scratch_numel * sizeof(float), /*alignment=*/alignof(float));
782+
if (scratch.ok()) {
783+
scratch_data = reinterpret_cast<float*>(scratch.get());
784+
} else {
785+
fallback_scratch = std::make_unique<float[]>(scratch_numel);
786+
scratch_data = fallback_scratch.get();
787+
}
788+
float* v_pred = scratch_data;
789+
float* delta = scratch_data + v_head_dim;
778790

779791
for (int64_t batch = 0; batch < batch_size; ++batch) {
780792
for (int64_t head = 0; head < num_heads; ++head) {
@@ -809,47 +821,44 @@ std::tuple<Tensor&, Tensor&> channelwise_gated_delta_rule_out(
809821
const float beta_t = beta_head[token];
810822
auto* output_t = output_head + token * value_seq_stride;
811823

812-
// 1. Per-key-channel decay: scale each state row by decay_t[k].
824+
// The recurrence needs only two passes over the K x V state S:
825+
// pass 1 (read-only): v_pred = (Diag(decay) S)^T k
826+
// pass 2 (read+write): S = Diag(decay) S + k (x) delta; o = S^T q
827+
// Decay is folded into both passes (never materialized separately), and
828+
// the rank-1 write and output readout share pass 2, so S is streamed
829+
// twice per token instead of four times. Multiply groupings match the
830+
// naive form, so results are identical up to floating-point contraction
831+
// (e.g. compiler FMA).
832+
833+
// Pass 1: predicted value off the decayed state (S left untouched).
834+
std::fill(v_pred, v_pred + v_head_dim, 0.0f);
813835
for (int64_t k_idx = 0; k_idx < k_head_dim; ++k_idx) {
814836
const float decay_value = decay_t[k_idx];
815-
auto* state_row = state_head + k_idx * v_head_dim;
816-
for (int64_t v_idx = 0; v_idx < v_head_dim; ++v_idx) {
817-
state_row[v_idx] *= decay_value;
818-
}
819-
}
820-
821-
// 2. v_pred = S^T k: the memory's predicted value for this key, read
822-
// off the decayed state.
823-
std::fill(v_pred.begin(), v_pred.end(), 0.0f);
824-
for (int64_t k_idx = 0; k_idx < k_head_dim; ++k_idx) {
825837
const float key_value = k_t[k_idx];
826838
const auto* state_row = state_head + k_idx * v_head_dim;
827839
for (int64_t v_idx = 0; v_idx < v_head_dim; ++v_idx) {
828-
v_pred[v_idx] += state_row[v_idx] * key_value;
840+
v_pred[v_idx] += (state_row[v_idx] * decay_value) * key_value;
829841
}
830842
}
831843

832-
// 3. delta = beta * (v - v_pred).
844+
// delta = beta * (v - v_pred).
833845
for (int64_t v_idx = 0; v_idx < v_head_dim; ++v_idx) {
834846
delta[v_idx] = (v_t[v_idx] - v_pred[v_idx]) * beta_t;
835847
}
836848

837-
// 4. Rank-1 write: S += k (x) delta.
838-
for (int64_t k_idx = 0; k_idx < k_head_dim; ++k_idx) {
839-
const float key_value = k_t[k_idx];
840-
auto* state_row = state_head + k_idx * v_head_dim;
841-
for (int64_t v_idx = 0; v_idx < v_head_dim; ++v_idx) {
842-
state_row[v_idx] += key_value * delta[v_idx];
843-
}
844-
}
845-
846-
// 5. Output readout: o_t = S^T q.
849+
// Pass 2: apply decay + rank-1 write in place, and read back the
850+
// updated row for the output projection in the same sweep.
847851
std::fill(output_t, output_t + v_head_dim, 0.0f);
848852
for (int64_t k_idx = 0; k_idx < k_head_dim; ++k_idx) {
853+
const float decay_value = decay_t[k_idx];
854+
const float key_value = k_t[k_idx];
849855
const float query_value = q_t[k_idx];
850-
const auto* state_row = state_head + k_idx * v_head_dim;
856+
auto* state_row = state_head + k_idx * v_head_dim;
851857
for (int64_t v_idx = 0; v_idx < v_head_dim; ++v_idx) {
852-
output_t[v_idx] += state_row[v_idx] * query_value;
858+
const float updated =
859+
state_row[v_idx] * decay_value + key_value * delta[v_idx];
860+
state_row[v_idx] = updated;
861+
output_t[v_idx] += updated * query_value;
853862
}
854863
}
855864
}

0 commit comments

Comments
 (0)