Skip to content

Commit a8260cd

Browse files
authored
[webgpu] Optimize LinearAttention Op with subgroup (#28412)
### Description Optimize the `LinearAttention` Op with subgroupAdd(). - Detect the adapter's `subgroupMinSize` to allocate workgroup shared memory - Drastically reduces workgroup shared memory usage (workgroup_size_x * TILE_V → MAX_SG * TILE_V) - Eliminates most `workgroupBarrier()` calls in the subgroup reduction The optimization is gated behind `subgroup_min_size`, which is enabled when the device supports `wgpu::FeatureName::Subgroups`. The original is preserved as fallback. ## Qwen3.5-4B Performance Benchmarks | Metric | Prefill Speed (TPS) | Decode Speed (TPS) | Prefill Improvement | | :--- | :--- | :--- | :--- | | **Default** | 719.6 | 29.6 | - | | **Optimized** | 929.8 | 29.7 | 1.29x | **Test Environment:** * **Hardware:** Intel Panther Lake * **Configuration:** Prefill: 1024, Decode: 128 ### Motivation and Context See above.
1 parent 480aad0 commit a8260cd

3 files changed

Lines changed: 137 additions & 5 deletions

File tree

onnxruntime/contrib_ops/webgpu/bert/linear_attention.cc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Status LinearAttentionProgram::GenerateShaderCode(ShaderHelper& shader) const {
8181
return WGSL_TEMPLATE_APPLY(shader, "bert/linear_attention.wgsl.template",
8282
WGSL_TEMPLATE_PARAMETER(decay_broadcast_dk, decay_broadcast_dk_),
8383
WGSL_TEMPLATE_PARAMETER(has_initial_state, has_initial_state_),
84+
WGSL_TEMPLATE_PARAMETER(subgroup_min_size, subgroup_min_size_),
8485
WGSL_TEMPLATE_PARAMETER(tile_v, tile_v_),
8586
WGSL_TEMPLATE_PARAMETER(update_rule, update_rule_int),
8687
WGSL_TEMPLATE_PARAMETER(use_vec4, use_vec4));
@@ -242,7 +243,12 @@ Status LinearAttention::ComputeInternal(ComputeContext& context) const {
242243
}
243244
}
244245

245-
LinearAttentionProgram program{update_rule_, has_initial_state, has_decay, has_beta, decay_broadcast_dk, tile_v, components};
246+
// subgroup_min_size > 0 enables subgroup-based reduction; 0 falls back to barrier-tree.
247+
int subgroup_min_size = context.HasFeature(wgpu::FeatureName::Subgroups)
248+
? static_cast<int>(context.AdapterInfo().subgroupMinSize)
249+
: 0;
250+
251+
LinearAttentionProgram program{update_rule_, has_initial_state, has_decay, has_beta, decay_broadcast_dk, tile_v, components, subgroup_min_size};
246252

247253
program.AddInputs({{query, ProgramTensorMetadataDependency::TypeAndRank},
248254
{key, ProgramTensorMetadataDependency::TypeAndRank},
@@ -263,7 +269,7 @@ Status LinearAttention::ComputeInternal(ComputeContext& context) const {
263269
program.SetDispatchGroupSize(num_workgroups)
264270
.SetWorkgroupSize(workgroup_size)
265271
.CacheHint(std::to_string(static_cast<int>(update_rule_)),
266-
has_initial_state, has_decay, has_beta, decay_broadcast_dk, tile_v, components)
272+
has_initial_state, has_decay, has_beta, decay_broadcast_dk, tile_v, components, subgroup_min_size)
267273
.AddUniformVariables({{static_cast<uint32_t>(batch_size)},
268274
{static_cast<uint32_t>(kv_num_heads_)},
269275
{static_cast<uint32_t>(seq_length)},

onnxruntime/contrib_ops/webgpu/bert/linear_attention.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,17 @@ LinearAttentionUpdateRule ParseUpdateRule(const std::string& rule_str);
3232
class LinearAttentionProgram final : public Program<LinearAttentionProgram> {
3333
public:
3434
LinearAttentionProgram(LinearAttentionUpdateRule update_rule, bool has_initial_state,
35-
bool has_decay, bool has_beta, bool decay_broadcast_dk, int tile_v, int components)
35+
bool has_decay, bool has_beta, bool decay_broadcast_dk, int tile_v, int components,
36+
int subgroup_min_size)
3637
: Program{"LinearAttention"},
3738
update_rule_(update_rule),
3839
has_initial_state_(has_initial_state),
3940
has_decay_(has_decay),
4041
has_beta_(has_beta),
4142
decay_broadcast_dk_(decay_broadcast_dk),
4243
tile_v_(tile_v),
43-
components_(components) {}
44+
components_(components),
45+
subgroup_min_size_(subgroup_min_size) {}
4446

4547
Status GenerateShaderCode(ShaderHelper& sh) const override;
4648

@@ -65,6 +67,7 @@ class LinearAttentionProgram final : public Program<LinearAttentionProgram> {
6567
bool decay_broadcast_dk_;
6668
int tile_v_;
6769
int components_;
70+
int subgroup_min_size_;
6871
};
6972

7073
// Kernel for LinearAttention

onnxruntime/contrib_ops/webgpu/bert/linear_attention.wgsl.template

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#param has_initial_state
2020
#param decay_broadcast_dk
2121
#param tile_v
22+
#param subgroup_min_size // 0 = disable subgroup reduction
2223

2324
// Update rule constants
2425
#define UPDATE_LINEAR 0
@@ -41,16 +42,29 @@ const TILE_V: u32 = tile_v;
4142

4243
// Shared memory for parallel reduction across dk threads.
4344
#if update_rule == UPDATE_DELTA || update_rule == UPDATE_GATED_DELTA
45+
#if subgroup_min_size
46+
const MAX_SG: u32 = (workgroup_size_x + subgroup_min_size - 1u) / subgroup_min_size;
47+
var<workgroup> sg_retrieved: array<vtype, MAX_SG * TILE_V>;
48+
var<workgroup> sg_preout: array<vtype, MAX_SG * TILE_V>;
49+
var<workgroup> sg_kq: array<f32, MAX_SG>;
50+
var<workgroup> broadcast_buf: array<vtype, TILE_V>;
51+
#else
4452
// Fused reduction: retrieved (S^T@k), pre_output (S^T@q), and kq_dot (k^T@q)
4553
// are reduced in a single barrier tree.
4654
var<workgroup> red_retrieved: array<vtype, workgroup_size_x * TILE_V>;
4755
var<workgroup> red_preout: array<vtype, workgroup_size_x * TILE_V>;
4856
var<workgroup> red_kq: array<f32, workgroup_size_x>;
4957
var<workgroup> broadcast_buf: array<vtype, TILE_V>;
58+
#endif
59+
#else
60+
#if subgroup_min_size
61+
const MAX_SG: u32 = (workgroup_size_x + subgroup_min_size - 1u) / subgroup_min_size;
62+
var<workgroup> sg_buf: array<vtype, MAX_SG * TILE_V>;
5063
#else
5164
// Output-only reduction for linear/gated.
5265
var<workgroup> reduction_buf: array<vtype, workgroup_size_x * TILE_V>;
5366
#endif
67+
#endif
5468

5569
$MAIN {
5670
// Identify which (batch, head, dv_tile) this workgroup handles
@@ -61,6 +75,11 @@ $MAIN {
6175
let dk_idx = local_idx; // thread index = row in state matrix
6276
let dv_start = dv_tile_idx * TILE_V;
6377

78+
#if subgroup_min_size
79+
let sg_idx = local_idx / sg_size;
80+
let num_sgs = (workgroup_size_x + sg_size - 1u) / sg_size;
81+
#endif
82+
6483
// Precompute packed strides for 3D packed inputs (B, T, H*D)
6584
// Q: (B, T, q_num_heads * dk), K: (B, T, n_k_heads * dk),
6685
// V/output: (B, T, num_heads * dv) [schema: output_dim == V_dim]
@@ -133,8 +152,47 @@ $MAIN {
133152
}
134153

135154
// Fused reduction: compute retrieved = S^T@k, pre_output = S^T@q_0,
136-
// and kq_dot = k^T@q_0 in a single barrier tree.
155+
// and kq_dot = k^T@q_0.
137156
// Then: output_0 = scale * (pre_output + delta * kq_dot)
157+
#if subgroup_min_size
158+
for (var j = 0u; j < TILE_V; j++) {
159+
let ret_j = subgroupAdd(state[j] * k_val);
160+
let pre_j = subgroupAdd(state[j] * q0_val);
161+
if (sg_id == 0u) {
162+
sg_retrieved[sg_idx * TILE_V + j] = ret_j;
163+
sg_preout[sg_idx * TILE_V + j] = pre_j;
164+
}
165+
}
166+
let kq_sg = subgroupAdd(k_val * q0_val);
167+
if (sg_id == 0u) {
168+
sg_kq[sg_idx] = kq_sg;
169+
}
170+
workgroupBarrier();
171+
172+
// Threads 0..TILE_V-1 each handle one dv position independently.
173+
let v_base = bt_offset * packed_dv + head_idx * uniforms.head_dim_v + dv_start;
174+
let beta_base = bt_offset * uniforms.num_heads + head_idx;
175+
if (dk_idx < TILE_V) {
176+
let j = dk_idx;
177+
var retrieved_sum = vtype(0.0);
178+
var preout_sum = vtype(0.0);
179+
var kq_dot: f32 = 0.0;
180+
for (var sg = 0u; sg < num_sgs; sg++) {
181+
retrieved_sum += sg_retrieved[sg * TILE_V + j];
182+
preout_sum += sg_preout[sg * TILE_V + j];
183+
kq_dot += sg_kq[sg];
184+
}
185+
let beta_val = f32(beta[beta_base]);
186+
if (dv_start + j < uniforms.head_dim_v) {
187+
let v_val = vtype(value[v_base + j]);
188+
let delta_j = beta_val * (v_val - retrieved_sum);
189+
broadcast_buf[j] = delta_j;
190+
output[bt_offset * packed_dv + out_head_0 * uniforms.head_dim_v + dv_start + j] = otype((preout_sum + delta_j * kq_dot) * uniforms.scale);
191+
} else {
192+
broadcast_buf[j] = vtype(0.0);
193+
}
194+
}
195+
#else
138196
for (var j = 0u; j < TILE_V; j++) {
139197
red_retrieved[j * workgroup_size_x + dk_idx] = state[j] * k_val;
140198
red_preout[j * workgroup_size_x + dk_idx] = state[j] * q0_val;
@@ -172,6 +230,7 @@ $MAIN {
172230
}
173231
}
174232
}
233+
#endif
175234
workgroupBarrier();
176235

177236
// All threads: update state with delta (S_new = S_old + k * delta)
@@ -188,6 +247,27 @@ $MAIN {
188247
if (dk_idx < uniforms.head_dim_k) {
189248
qg_val = f32(query[bt_offset * packed_dk_q + q_head_g * uniforms.head_dim_k + dk_idx]);
190249
}
250+
#if subgroup_min_size
251+
// Subgroup reduction of state*qg
252+
for (var j = 0u; j < TILE_V; j++) {
253+
let pre_j = subgroupAdd(state[j] * qg_val);
254+
if (sg_id == 0u) {
255+
sg_preout[sg_idx * TILE_V + j] = pre_j;
256+
}
257+
}
258+
workgroupBarrier();
259+
// Cross-subgroup reduction + parallel output write
260+
if (dk_idx < TILE_V) {
261+
let j = dk_idx;
262+
var preout_sum = vtype(0.0);
263+
for (var sg = 0u; sg < num_sgs; sg++) {
264+
preout_sum += sg_preout[sg * TILE_V + j];
265+
}
266+
if (dv_start + j < uniforms.head_dim_v) {
267+
output[bt_offset * packed_dv + q_head_g * uniforms.head_dim_v + dv_start + j] = otype(preout_sum * uniforms.scale);
268+
}
269+
}
270+
#else
191271
for (var j = 0u; j < TILE_V; j++) {
192272
red_preout[j * workgroup_size_x + dk_idx] = state[j] * qg_val;
193273
}
@@ -207,6 +287,7 @@ $MAIN {
207287
}
208288
}
209289
}
290+
#endif
210291
workgroupBarrier();
211292
}
212293

@@ -228,6 +309,26 @@ $MAIN {
228309
if (dk_idx < uniforms.head_dim_k) {
229310
q_val_g = f32(query[bt_offset * packed_dk_q + q_head_idx * uniforms.head_dim_k + dk_idx]);
230311
}
312+
#if subgroup_min_size
313+
for (var j = 0u; j < TILE_V; j++) {
314+
let pre_j = subgroupAdd(state[j] * q_val_g);
315+
if (sg_id == 0u) {
316+
sg_buf[sg_idx * TILE_V + j] = pre_j;
317+
}
318+
}
319+
workgroupBarrier();
320+
if (dk_idx < TILE_V) {
321+
let j = dk_idx;
322+
var preout_sum = vtype(0.0);
323+
for (var sg = 0u; sg < num_sgs; sg++) {
324+
preout_sum += sg_buf[sg * TILE_V + j];
325+
}
326+
let out_base = bt_offset * packed_dv + q_head_idx * uniforms.head_dim_v + dv_start;
327+
if (dv_start + j < uniforms.head_dim_v) {
328+
output[out_base + j] = otype(preout_sum * uniforms.scale);
329+
}
330+
}
331+
#else
231332
for (var j = 0u; j < TILE_V; j++) {
232333
reduction_buf[j * workgroup_size_x + dk_idx] = state[j] * q_val_g;
233334
}
@@ -248,6 +349,7 @@ $MAIN {
248349
}
249350
}
250351
}
352+
#endif
251353
workgroupBarrier();
252354
}
253355
} else {
@@ -257,6 +359,26 @@ $MAIN {
257359
if (dk_idx < uniforms.head_dim_k) {
258360
q_val_inv = f32(query[bt_offset * packed_dk_q + q_head_inv * uniforms.head_dim_k + dk_idx]);
259361
}
362+
#if subgroup_min_size
363+
for (var j = 0u; j < TILE_V; j++) {
364+
let pre_j = subgroupAdd(state[j] * q_val_inv);
365+
if (sg_id == 0u) {
366+
sg_buf[sg_idx * TILE_V + j] = pre_j;
367+
}
368+
}
369+
workgroupBarrier();
370+
if (dk_idx < TILE_V) {
371+
let j = dk_idx;
372+
var preout_sum = vtype(0.0);
373+
for (var sg = 0u; sg < num_sgs; sg++) {
374+
preout_sum += sg_buf[sg * TILE_V + j];
375+
}
376+
let out_base = bt_offset * packed_dv + head_idx * uniforms.head_dim_v + dv_start;
377+
if (dv_start + j < uniforms.head_dim_v) {
378+
output[out_base + j] = otype(preout_sum * uniforms.scale);
379+
}
380+
}
381+
#else
260382
for (var j = 0u; j < TILE_V; j++) {
261383
reduction_buf[j * workgroup_size_x + dk_idx] = state[j] * q_val_inv;
262384
}
@@ -277,6 +399,7 @@ $MAIN {
277399
}
278400
}
279401
}
402+
#endif
280403
workgroupBarrier();
281404
}
282405
#endif

0 commit comments

Comments
 (0)