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.
4654var<workgroup> red_retrieved: array<vtype, workgroup_size_x * TILE_V>;
4755var<workgroup> red_preout: array<vtype, workgroup_size_x * TILE_V>;
4856var<workgroup> red_kq: array<f32, workgroup_size_x>;
4957var<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.
5265var<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