Skip to content

Commit 5ee2d71

Browse files
authored
[ET-VK][sdpa] Vendor-adaptive head_dim output-tiling (TILE_N4=2) in GQA AV coop-GEMV
Differential Revision: D112906313 Pull Request resolved: #21064
1 parent 014e53f commit 5ee2d71

5 files changed

Lines changed: 109 additions & 24 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,11 @@ sdpa_compute_out_coop:
2626
- NAME: sdpa_compute_out_coop
2727
- NAME: sdpa_compute_out_gqa_coop
2828
GQA: True
29+
# head_dim output-tiled GQA variant: each workgroup owns TILE_N4 head_dim
30+
# texels, amortizing the attn-weight loads and shared-memory reduction over
31+
# G x TILE_N4 outputs. A win on Adreno but a regression on Mali at common
32+
# decode contexts, so it is selected vendor-adaptively (Adreno only) in
33+
# SDPA.cpp; the partial_n_tile guard handles D4 % TILE_N4 != 0.
34+
- NAME: sdpa_compute_out_gqa_coop_tile2
35+
GQA: True
36+
TILE_N4: 2

backends/vulkan/runtime/graph/ops/impl/SDPA.cpp

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,9 @@ bool use_gqa_av_coop(
190190
}
191191

192192
// Resolve whether the AV decode path uses the GQA-reuse coop shader, honoring
193-
// the test-only shader_override knob (see SDPA.h): -1/kDummyValueRef
194-
// auto-selects via use_gqa_av_coop; 0 forces off; 1 forces on.
193+
// the test-only shader_override knob (see SDPA.h): auto selects via
194+
// use_gqa_av_coop; kShaderOverrideForceNonGqa forces off; any other value
195+
// forces the GQA family on.
195196
bool resolve_use_gqa(
196197
ComputeGraph* graph,
197198
const ValueRef shader_override,
@@ -200,20 +201,38 @@ bool resolve_use_gqa(
200201
if (shader_override == kDummyValueRef) {
201202
return use_gqa_av_coop(graph, num_q_heads, num_kv_heads);
202203
}
203-
const bool force_gqa = graph->extract_scalar<int64_t>(shader_override) != 0;
204+
const bool force_gqa = graph->extract_scalar<int64_t>(shader_override) !=
205+
kShaderOverrideForceNonGqa;
204206
// Forcing the GQA shader on an ineligible shape would silently drop query
205207
// heads (z-dispatch = Hkv cannot cover Hq, or group size exceeds the shader's
206208
// fixed accumulator array). Fail loudly instead of producing garbage output.
207209
if (force_gqa) {
208210
VK_CHECK_COND(
209211
num_kv_heads > 0 && num_q_heads % num_kv_heads == 0 &&
210212
num_q_heads / num_kv_heads <= kMaxGqaGroupSize,
211-
"shader_override=1 requires a GQA-eligible shape: Hq divisible by Hkv and "
212-
"group size <= kMaxGqaGroupSize");
213+
"forcing GQA via shader_override requires a GQA-eligible shape: Hq "
214+
"divisible by Hkv and group size <= kMaxGqaGroupSize");
213215
}
214216
return force_gqa;
215217
}
216218

219+
// Resolve whether the GQA-reuse AV path uses the head_dim output-tiled (_tile2)
220+
// variant. Auto (no override) and kShaderOverrideForceGqa defer to the vendor
221+
// gate (tile2 on Adreno); the force values pin the variant on any device (see
222+
// SDPA.h), giving the Adreno-only tile2 variant deterministic test coverage.
223+
bool resolve_use_tile2(ComputeGraph* graph, const ValueRef shader_override) {
224+
if (shader_override != kDummyValueRef) {
225+
const int64_t ov = graph->extract_scalar<int64_t>(shader_override);
226+
if (ov == kShaderOverrideForceTile2) {
227+
return true;
228+
}
229+
if (ov == kShaderOverrideForceBase) {
230+
return false;
231+
}
232+
}
233+
return graph->device_is_adreno();
234+
}
235+
217236
vkapi::ShaderInfo pick_sdpa_qk_shader(
218237
ComputeGraph* graph,
219238
const std::vector<ArgGroup>& args,
@@ -347,6 +366,16 @@ vkapi::ShaderInfo pick_sdpa_av_shader(
347366
// reusing it across the group. Cuts the dominant V-cache traffic ~Gx
348367
// for this bandwidth-bound kernel.
349368
shader_name += "_gqa_coop";
369+
// The _tile2 (head_dim output-tiled) variant amortizes the attn-weight
370+
// loads and shared-memory reduction over more outputs per workgroup. A
371+
// consistent win on Adreno (AV ~1.14-1.63x) but a regression on Mali at
372+
// common decode contexts (~0.67-0.86x, interleaved median), so it is
373+
// selected vendor-adaptively (tests can pin it via shader_override; see
374+
// resolve_use_tile2). pick_sdpa_av_global_wg_size keys the x-dim
375+
// collapse off this same _tile2 suffix.
376+
if (resolve_use_tile2(graph, shader_override)) {
377+
shader_name += "_tile2";
378+
}
350379
} else {
351380
shader_name += "_coop";
352381
}
@@ -389,7 +418,14 @@ utils::uvec3 pick_sdpa_av_global_wg_size(
389418
if (shader.kernel_name.find("_gqa_coop") != std::string::npos) {
390419
const ValueRef v = args.at(1).refs.at(1);
391420
const uint32_t num_kv_heads = graph->size_at<uint32_t>(-2, v);
392-
return {N4, M4, static_cast<uint32_t>(num_kv_heads * d.B)};
421+
// The _tile2 variant has each workgroup own 2 head_dim texels, so its x-dim
422+
// collapses to div_up(D4, 2). The plain (TILE_N4 == 1) variant keeps x ==
423+
// D4.
424+
const uint32_t x_dim =
425+
shader.kernel_name.find("_tile2") != std::string::npos
426+
? utils::div_up(N4, 2u)
427+
: N4;
428+
return {x_dim, M4, static_cast<uint32_t>(num_kv_heads * d.B)};
393429
}
394430

395431
return {N4, M4, static_cast<uint32_t>(d.H * d.B)};

backends/vulkan/runtime/graph/ops/impl/SDPA.h

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,31 @@ void add_sdpa_attn_weights_softmax_node(
4444
const ValueRef attn_weights_softmax,
4545
const SDPAMode mode);
4646

47+
// Scalar values for the shader_override knob (see add_sdpa_compute_out_node).
48+
constexpr int64_t kShaderOverrideForceNonGqa = 0;
49+
constexpr int64_t kShaderOverrideForceGqa = 1;
50+
constexpr int64_t kShaderOverrideForceTile2 = 2;
51+
constexpr int64_t kShaderOverrideForceBase = 3;
52+
4753
// shader_override: test-only knob selecting the single-token decode AV shader.
48-
// kDummyValueRef (-1) — auto: use the GQA-reuse coop shader
49-
// (`sdpa_compute_out_gqa_coop`) when it applies.
50-
// 0 — force the per-query-head coop shader
51-
// (`sdpa_compute_out_coop`).
52-
// 1 — force the GQA-reuse coop shader.
53-
// Lets the benchmark/test exercise both AV shaders on the same shape. Forcing
54-
// GQA (1) requires a GQA-eligible shape (Hq divisible by Hkv, group size <= the
55-
// shader's compile-time bound); this is VK_CHECK'd, so an ineligible forced
56-
// shape fails loudly rather than silently dropping query heads.
54+
// It is a ValueRef holding one of the kShaderOverride* int64 scalars above, or
55+
// kDummyValueRef for "no override" (auto).
56+
// auto (kDummyValueRef) — use the GQA-reuse coop shader when it applies,
57+
// with the tiled vs base variant chosen by vendor.
58+
// kShaderOverrideForceNonGqa — force the per-query-head coop shader
59+
// (`sdpa_compute_out_coop`).
60+
// kShaderOverrideForceGqa — force the GQA-reuse coop shader; vendor picks
61+
// the
62+
// base (`sdpa_compute_out_gqa_coop`) vs head_dim
63+
// output-tiled (`sdpa_compute_out_gqa_coop_tile2`)
64+
// variant (tile2 on Adreno).
65+
// kShaderOverrideForceTile2 — force the tiled variant regardless of vendor.
66+
// kShaderOverrideForceBase — force the base variant regardless of vendor.
67+
// Lets the benchmark/test exercise every AV shader on the same shape, and gives
68+
// the Adreno-only tile2 variant deterministic coverage on any device. Forcing
69+
// the GQA family requires a GQA-eligible shape (Hq divisible by Hkv, group size
70+
// <= the shader's compile-time bound); this is VK_CHECK'd, so an ineligible
71+
// forced shape fails loudly rather than silently dropping query heads.
5772
void add_sdpa_compute_out_node(
5873
ComputeGraph& graph,
5974
const ValueRef attn_weights_softmax,

backends/vulkan/test/custom_ops/impl/TestSDPA.cpp

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,11 @@ static void test_sdpa_impl(
119119
// Decode vs prefill is selected automatically inside the nodes via
120120
// is_single_token() (S == 1 -> coop/GEMV shaders, S > 1 -> tiled shaders). For
121121
// single-token decode the AV shader is picked by impl_selector:
122-
// "default" -> auto-select (GQA-reuse coop shader when it applies)
123-
// "gqa" -> force the GQA-reuse coop shader
124-
// "non_gqa" -> force the per-query-head coop shader
122+
// "default" -> auto-select (GQA-reuse coop shader when it applies)
123+
// "gqa" -> force the GQA-reuse coop shader (vendor picks base vs tile2)
124+
// "gqa_tile2" -> force the head_dim output-tiled GQA variant (any device)
125+
// "gqa_base" -> force the base (non-tiled) GQA variant (any device)
126+
// "non_gqa" -> force the per-query-head coop shader
125127
// impl_selector has no effect on prefill (tiled).
126128
//
127129
// Args: q, k_cache, v_cache, impl_selector, out
@@ -136,8 +138,10 @@ void test_sdpa(ComputeGraph& graph, const std::vector<ValueRef>& args) {
136138
const std::string impl_selector = graph.extract_string(impl_selector_str);
137139
VK_CHECK_COND(
138140
impl_selector == "default" || impl_selector == "gqa" ||
141+
impl_selector == "gqa_tile2" || impl_selector == "gqa_base" ||
139142
impl_selector == "non_gqa",
140-
"test_sdpa: impl_selector must be one of {default, gqa, non_gqa}");
143+
"test_sdpa: impl_selector must be one of {default, gqa, gqa_tile2, "
144+
"gqa_base, non_gqa}");
141145

142146
const int64_t seq_len = graph.size_at<int64_t>(-3, q);
143147
const int64_t context_len = graph.size_at<int64_t>(-3, k_cache);
@@ -147,12 +151,17 @@ void test_sdpa(ComputeGraph& graph, const std::vector<ValueRef>& args) {
147151

148152
const ValueRef input_pos_symint = graph.add_symint(input_pos_val);
149153

150-
// shader_override (see SDPA.h): -1 auto, 0 force non-GQA, 1 force GQA.
154+
// shader_override (see SDPA.h): kDummyValueRef auto; otherwise a
155+
// kShaderOverride* scalar forcing the AV shader family / variant.
151156
ValueRef shader_override = kDummyValueRef;
152157
if (impl_selector == "gqa") {
153-
shader_override = graph.add_scalar<int64_t>(1);
158+
shader_override = graph.add_scalar<int64_t>(kShaderOverrideForceGqa);
159+
} else if (impl_selector == "gqa_tile2") {
160+
shader_override = graph.add_scalar<int64_t>(kShaderOverrideForceTile2);
161+
} else if (impl_selector == "gqa_base") {
162+
shader_override = graph.add_scalar<int64_t>(kShaderOverrideForceBase);
154163
} else if (impl_selector == "non_gqa") {
155-
shader_override = graph.add_scalar<int64_t>(0);
164+
shader_override = graph.add_scalar<int64_t>(kShaderOverrideForceNonGqa);
156165
}
157166

158167
test_sdpa_impl(

backends/vulkan/test/custom_ops/test_sdpa.cpp

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,8 @@ static std::vector<TestCase> generate_sdpa_test_cases() {
309309
// attn_weights S/context alignment (a decode-shaped buffer allocation has no
310310
// headroom for the shaders' align_up_4 stride unless padded — see sdpa_impl).
311311
{
312-
// Cover both D=64 (D4=16) and D=128 (D4=32) so the GQA AV path is
313-
// validated across head_dim.
312+
// Cover D=64 (D4=16) and D=128 (D4=32) with the vendor-default GQA and the
313+
// per-query-head shaders, across texture + buffer.
314314
const std::vector<SDPAConfig> decs = {
315315
{64, 8, 2, 1, 32, "accu", "decode"},
316316
{128, 8, 2, 1, 32, "accu_d128", "decode"},
@@ -323,6 +323,23 @@ static std::vector<TestCase> generate_sdpa_test_cases() {
323323
}
324324
}
325325
}
326+
327+
// Force the head_dim output-tiled GQA variant (Adreno-only in production)
328+
// so its wg x-collapse and the partial_n_tile tail get deterministic
329+
// coverage on any device: D=64/128 give even D4 (fast path); D=4 gives D4=1
330+
// (odd), exercising the partial-tile checked load.
331+
const std::vector<SDPAConfig> tile2_decs = {
332+
{64, 8, 2, 1, 32, "accu_tile2", "decode"},
333+
{128, 8, 2, 1, 32, "accu_tile2_d128", "decode"},
334+
{4, 8, 2, 1, 32, "accu_tile2_d4", "decode"},
335+
};
336+
for (const auto& dec : tile2_decs) {
337+
for (const auto& storage : {utils::kTexture3D, utils::kBuffer}) {
338+
test_cases.push_back(
339+
create_sdpa_test_case(dec, vkapi::kFloat, storage, "gqa_tile2"));
340+
}
341+
}
342+
326343
SDPAConfig pre{64, 8, 2, 16, 16, "accu", "prefill"};
327344
test_cases.push_back(create_sdpa_test_case(
328345
pre, vkapi::kFloat, utils::kTexture3D, "default"));

0 commit comments

Comments
 (0)