Skip to content

Commit bd7991f

Browse files
committed
[ET-VK][sdpa] Reuse shared V cache across GQA query heads in AV coop-GEMV
Pull Request resolved: #21063 The LLM decode AV coop-GEMV reloads the shared V cache once per query head. In grouped-query attention Hq = G * Hkv query heads share each KV head (Llama G=4, Phi G=3, Qwen G=2), and out[q_h, d] = sum_c attn[c, q_h] * V[c, kv_h, d] reads the SAME V texel for every query head in a group. The per-query-head coop shader gives each of the Hq heads its own workgroup, so V -- the dominant traffic (head_dim-wide per context texel, vs a scalar attn weight) -- is read G times. This adds a GQA-reuse AV variant that assigns ONE workgroup per (d4, kv_h): it loads each V texel once and reuses it across all G query heads in the group, producing G output texels. For this bandwidth-bound kernel that cuts V-cache traffic ~Gx. Implementation: - The variant is a codegen flag (`GQA`) on the existing `sdpa_compute_out_coop.glsl` template, not a separate file: one shared header plus two `#ifdef GQA` `main()`s (per-head and GQA-reuse), so the shared setup lives in one place while each algorithm reads end-to-end. It emits the shader `sdpa_compute_out_gqa_coop`. - Reduction reuses the per-head coop shader's shared-memory tree reduction (no subgroup arithmetic), so the variant runs on any Vulkan device -- Adreno and Mali alike -- with no capability gate. - Each thread holds G output accumulators; the array is sized to a compile-time `MAX_GROUP_SIZE` = 8 and the group loop is bounded by the `group_size` = Hq/Hkv spec constant, so the driver fully unrolls it at pipeline creation. - Dispatch (`pick_sdpa_av_shader` + global-wg picker + spec-const wiring in `add_sdpa_compute_out_node`): the GQA variant is selected on the LLM decode coop path when Hq > Hkv, evenly divisible, and G <= 8 (`use_gqa_av_coop`); it sets `group_size` and changes the global workgroup z-dim from Hq to Hkv. Everything else -- MHA (Hq == Hkv), groups exceeding the cap (G > 8, e.g. MQA with Hq > 8), and non-divisible shapes -- falls back to the unchanged per-head `sdpa_compute_out_coop`. (Low-ratio MQA -- Hkv == 1 with Hq <= 8 -- is eligible and takes the GQA path.) - A test-only `gqa_override` knob is threaded through `add_sdpa_compute_out_node` (declared in the new `SDPA.h`): -1 auto-select, 0 force per-head, 1 force GQA, so a benchmark can exercise both AV shaders on the same shape; forcing GQA is VK_CHECK'd against shape eligibility. ghstack-source-id: 405400503 @exported-using-ghexport Differential Revision: [D112906311](https://our.internmc.facebook.com/intern/diff/D112906311/)
1 parent ff9b4c5 commit bd7991f

7 files changed

Lines changed: 547 additions & 66 deletions

File tree

backends/vulkan/runtime/graph/ops/glsl/sdpa_compute_out_coop.glsl

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ $if V_CACHE_STORAGE == "buffer":
3232

3333
#define NUM_WORKERS_PER_OUT 64
3434

35+
$if GQA:
36+
#define GQA
37+
// Maximum grouped-query-attention group size (Hq / Hkv) supported by the GQA
38+
// variant. The actual group size G is passed as a specialization constant and
39+
// must satisfy G <= MAX_GROUP_SIZE. The accumulator array is sized to
40+
// MAX_GROUP_SIZE (a compile-time constant) and the group loop is bounded by
41+
// the spec-const G so the driver can fully unroll it at pipeline creation
42+
// time. Group sizes seen in practice: Llama G=4, Phi G=3, Qwen G=2.
43+
#define MAX_GROUP_SIZE 8
44+
3545
${define_required_extensions(IO_STORAGE, DTYPE)}
3646

3747
layout(std430) buffer;
@@ -49,6 +59,8 @@ ${layout_declare_ubo(B, "int", "input_pos")}
4959
layout(local_size_x_id = 0, local_size_y_id = 1, local_size_z_id = 2) in;
5060

5161
${layout_declare_spec_const(C, "float", "inv_scale", "1.0")}
62+
$if GQA:
63+
${layout_declare_spec_const(C, "int", "group_size", "1")}
5264

5365
#include "sdpa_fp_attn_weight_tile_load.glslh"
5466
#include "sdpa_fp_v_cache_tile_load.glslh"
@@ -57,6 +69,177 @@ ${layout_declare_spec_const(C, "float", "inv_scale", "1.0")}
5769

5870
shared FPOutTile partial_sums[NUM_WORKERS_PER_OUT];
5971

72+
#ifdef GQA
73+
74+
/*
75+
* GQA-reuse variant of the AV coop GEMV.
76+
*
77+
* Grouped-query attention shares one KV head across G = Hq / Hkv query heads.
78+
* The AV computation out[q_h, d] = sum_c attn[c, q_h] * V[c, kv_h(q_h), d]
79+
* reads the SAME V cache for every query head in a group. In the per-query-head
80+
* coop variant each of the Hq heads gets its own workgroup and independently
81+
* re-loads the shared V cache, so V (the dominant traffic — head_dim wide per
82+
* context texel) is read G times. This variant assigns ONE workgroup per
83+
* (d4, kv_h): it loads each V texel once and reuses it across all G query heads
84+
* in the group, producing G output texels. This cuts V-cache traffic ~Gx for a
85+
* bandwidth-bound kernel. Dispatch sets the global wg z-dim to Hkv (not Hq).
86+
*/
87+
88+
void main() {
89+
const int worker_id = int(gl_LocalInvocationID.y);
90+
91+
const int tile_idx_x = int(gl_GlobalInvocationID.x);
92+
// idx along the K/V head dim
93+
const int kv_h = int(gl_GlobalInvocationID.z);
94+
95+
// idx along the output head_dim dim
96+
const int d = tile_idx_x * TILE_N;
97+
const int d4 = div_4(d);
98+
99+
// idx along the output seq_len dim. Note that for this shader seq_len will be
100+
// 1.
101+
const int s = 0;
102+
103+
// texel size of head_dim
104+
const int D4 = div_up_4(q_sizes.x);
105+
// number of Q heads
106+
const int Q_H = q_sizes.y;
107+
// sequence length
108+
const int S = q_sizes.z;
109+
const int S_aligned = align_up_4(S);
110+
111+
// number of K/V heads
112+
const int KV_H = v_sizes.y;
113+
// Max context length
114+
const int C = v_sizes.z;
115+
const int C4 = div_up_4(C);
116+
117+
const int G = group_size;
118+
// First query head in this group.
119+
const int q_h_base = kv_h * G;
120+
121+
// current context length
122+
const int context_len = input_pos + S;
123+
const int context_texel_len = div_up_4(context_len);
124+
125+
// bounds check
126+
if (d4 >= D4 || s >= S || kv_h >= KV_H) {
127+
return;
128+
}
129+
130+
// With head_dim output-tiling (TILE_N4 > 1) each workgroup owns TILE_N4
131+
// head_dim texels. Whether this workgroup owns a partial tile (only possible
132+
// on the final tile when D4 % TILE_N4 != 0). Uniform across the workgroup, so
133+
// the branch selecting the checked vs unchecked V load below is a uniform
134+
// branch. For the common even-D4 case (D=64 -> D4=16, D=128 -> D4=32, and
135+
// always for TILE_N4 == 1) this is false and the fast unchecked path is taken.
136+
const bool partial_n_tile = (d4 + TILE_N4) > D4;
137+
138+
FPOutTile out_tile[MAX_GROUP_SIZE];
139+
[[unroll]] for (int g = 0; g < MAX_GROUP_SIZE; ++g) {
140+
if (g >= G) {
141+
break;
142+
}
143+
initialize(out_tile[g]);
144+
}
145+
146+
FPInputTile attn_weight_tile;
147+
FPWeightTile w_tile;
148+
149+
const int context_len_aligned_down = context_len - mod_4(context_len);
150+
const int C4_limit = div_up_4(context_len_aligned_down);
151+
152+
// Main loop: each thread strides over context texels. The single V load per
153+
// texel is reused across all G query heads in the group.
154+
for (int c4 = worker_id; c4 < C4_limit; c4 += NUM_WORKERS_PER_OUT) {
155+
const int c = mul_4(c4);
156+
157+
// The TILE_N4 V texels are loaded once per context texel and reused across
158+
// all G query heads in the group. Bounds-check the head_dim index only on a
159+
// partial final tile (uniform branch, see partial_n_tile).
160+
if (partial_n_tile) {
161+
load_v_cache_tile_with_checks(
162+
w_tile, d4, c, kv_h, D4, context_len, C, KV_H);
163+
} else {
164+
load_v_cache_tile_no_checks(
165+
w_tile, d4, c, kv_h, D4, context_len, C, KV_H);
166+
}
167+
168+
[[unroll]] for (int g = 0; g < MAX_GROUP_SIZE; ++g) {
169+
if (g >= G) {
170+
break;
171+
}
172+
load_attn_weight_tile_no_checks(
173+
attn_weight_tile,
174+
c4,
175+
s,
176+
q_h_base + g,
177+
context_texel_len,
178+
S_aligned,
179+
Q_H);
180+
fp_accumulate_with_fp_weight(out_tile[g], attn_weight_tile, w_tile);
181+
}
182+
}
183+
184+
// first worker in the work group will handle final texel, which may contain
185+
// padding elements.
186+
if (worker_id == 0) {
187+
for (int c4 = C4_limit; c4 < context_texel_len; c4++) {
188+
const int c = mul_4(c4);
189+
190+
load_v_cache_tile_with_checks(
191+
w_tile, d4, c, kv_h, D4, context_len, C, KV_H);
192+
193+
[[unroll]] for (int g = 0; g < MAX_GROUP_SIZE; ++g) {
194+
if (g >= G) {
195+
break;
196+
}
197+
load_attn_weight_tile_with_checks(
198+
attn_weight_tile,
199+
c4,
200+
s,
201+
q_h_base + g,
202+
context_texel_len,
203+
S_aligned,
204+
Q_H);
205+
fp_accumulate_with_fp_weight(out_tile[g], attn_weight_tile, w_tile);
206+
}
207+
}
208+
}
209+
210+
// Combine the per-worker partial sums for each group with a shared-memory tree
211+
// reduction. partial_sums is reused across groups; no trailing barrier is
212+
// needed because each worker only writes its own slot and the next group's
213+
// leading barrier orders those writes after this group's reduction reads
214+
// (worker 0's store reads only slot 0, which only worker 0 writes).
215+
[[unroll]] for (int g = 0; g < MAX_GROUP_SIZE; ++g) {
216+
if (g >= G) {
217+
break;
218+
}
219+
partial_sums[worker_id] = out_tile[g];
220+
221+
memoryBarrierShared();
222+
barrier();
223+
224+
for (int i = NUM_WORKERS_PER_OUT / 2; i > 0; i /= 2) {
225+
if (worker_id < i) {
226+
accumulate_out_tile_with_out_tile(
227+
partial_sums[worker_id], partial_sums[worker_id + i]);
228+
}
229+
memoryBarrierShared();
230+
barrier();
231+
}
232+
233+
if (worker_id == 0) {
234+
out_tile[g] = partial_sums[0];
235+
store_sdpa_out_tile_with_checks(
236+
out_tile[g], d4, s, q_h_base + g, D4, S, Q_H);
237+
}
238+
}
239+
}
240+
241+
#else
242+
60243
/*
61244
* See the tiled variant of this shader for the implemented behavior. This
62245
* shader is implements an optimization for cases where sequence length is 1; in
@@ -197,3 +380,5 @@ void main() {
197380
Q_H);
198381
}
199382
}
383+
384+
#endif // GQA

backends/vulkan/runtime/graph/ops/glsl/sdpa_compute_out_coop.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ sdpa_compute_out_coop:
1111
V_CACHE_STORAGE: texture3d
1212
TILE_K4: 1
1313
TILE_N4: 1
14+
GQA: False
1415
generate_variant_forall:
1516
combination:
1617
parameter_names: [IO_STORAGE, V_CACHE_STORAGE]
@@ -23,3 +24,5 @@ sdpa_compute_out_coop:
2324
- VALUE: half
2425
shader_variants:
2526
- NAME: sdpa_compute_out_coop
27+
- NAME: sdpa_compute_out_gqa_coop
28+
GQA: True

0 commit comments

Comments
 (0)