Skip to content

Commit b0c6f38

Browse files
committed
Vulkan: support attn_mask and scale in llama.custom_sdpa / sdpa_with_kv_cache
The LLM SDPA path rejected both arguments at load. Sliding window attention (e.g. Gemma) needs them: an additive mask in place of the causal one, and a scale other than 1/sqrt(head_dim). As in the CPU op, the mask is a 2D [S, C] tensor over every key in the cache and cannot be combined with is_causal. The QK shaders get _mask variants that add it instead of applying the causal mask; softmax and AV are given C - S in place of input_pos so they attend to all C keys, which needs no shader change.
1 parent ad434a3 commit b0c6f38

7 files changed

Lines changed: 341 additions & 16 deletions

File tree

‎backends/vulkan/runtime/graph/ops/glsl/sdpa_compute_attn_weights_coop.glsl‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ $if IO_STORAGE == "buffer":
1818
#define ATTN_WEIGHTS_BUFFER
1919
$if K_CACHE_STORAGE == "buffer":
2020
#define K_CACHE_BUFFER
21+
$if HAS_MASK:
22+
#define HAS_MASK
2123

2224
#define Q_LAYOUT DHSB
2325
#define K_LAYOUT DHSB
@@ -40,10 +42,14 @@ layout(std430) buffer;
4042
${layout_declare_tensor(B, "w", "t_attn_weights", DTYPE, IO_STORAGE, is_scalar_array=False)}
4143
${layout_declare_tensor(B, "r", "t_q", DTYPE, IO_STORAGE, is_scalar_array=False)}
4244
${layout_declare_tensor(B, "r", "t_k", DTYPE, K_CACHE_STORAGE, is_scalar_array=False)}
45+
$if HAS_MASK:
46+
${layout_declare_tensor(B, "r", "t_mask", DTYPE, IO_STORAGE, is_scalar_array=True)}
4347

4448
${layout_declare_ubo(B, "ivec4", "q_sizes")}
4549
${layout_declare_ubo(B, "ivec4", "k_sizes")}
4650
${layout_declare_ubo(B, "int", "input_pos")}
51+
$if HAS_MASK:
52+
${layout_declare_ubo(B, "ivec4", "mask_sizes")}
4753

4854
layout(local_size_x_id = 0, local_size_y_id = 1, local_size_z_id = 2) in;
4955

@@ -118,7 +124,12 @@ void main() {
118124
// If the tile is completely inside the mask region, then there is no need to
119125
// compute the output tile. All the elements in the output tile can be set to
120126
// negative infinity.
127+
#ifdef HAS_MASK
128+
// The additive attn_mask replaces the causal mask, so no tile can be skipped.
129+
const bool tile_in_mask_region = false;
130+
#else
121131
bool tile_in_mask_region = c > (input_pos + s + (TILE_M - 1));
132+
#endif
122133
if (tile_in_mask_region) {
123134
const VEC4_T negative_infinity_vec = VEC4_T(negative_infinity_val);
124135
set_out_tile_to_vec(out_tile, negative_infinity_vec);
@@ -171,13 +182,18 @@ void main() {
171182
out_tile = partial_sums[0];
172183
// Apply scale and mask if the tile was not entirely in the mask region
173184
if (!tile_in_mask_region) {
185+
#ifdef HAS_MASK
186+
apply_scale(out_tile, inv_scale);
187+
apply_attn_mask(out_tile, c, s);
188+
#else
174189
VEC4_T inv_scale_vec = VEC4_T(inv_scale);
175190
apply_scale_and_mask(
176191
out_tile,
177192
inv_scale_vec,
178193
input_pos,
179194
c,
180195
s);
196+
#endif
181197
}
182198

183199
store_attn_weight_tile_with_checks(

‎backends/vulkan/runtime/graph/ops/glsl/sdpa_compute_attn_weights_coop.yaml‎

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

‎backends/vulkan/runtime/graph/ops/glsl/sdpa_compute_attn_weights_tiled.glsl‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ $else:
3636

3737
$if HAS_BIAS:
3838
#define HAS_BIAS
39+
$if HAS_MASK:
40+
#define HAS_MASK
3941

4042
#define TILE_M4 ${TILE_M4}
4143
#define TILE_K4 ${TILE_K4}
@@ -56,13 +58,17 @@ ${layout_declare_tensor(B, "r", "t_q", IN_DTYPE, IO_STORAGE, is_scalar_array=Fal
5658
${layout_declare_tensor(B, "r", "t_k", IN_DTYPE, K_CACHE_STORAGE, is_scalar_array=False)}
5759
$if HAS_BIAS:
5860
${layout_declare_tensor(B, "r", "t_bias", IN_DTYPE, IO_STORAGE, is_scalar_array=False)}
61+
$if HAS_MASK:
62+
${layout_declare_tensor(B, "r", "t_mask", IN_DTYPE, IO_STORAGE, is_scalar_array=True)}
5963

6064
${layout_declare_ubo(B, "ivec4", "q_sizes")}
6165
${layout_declare_ubo(B, "ivec4", "k_sizes")}
6266
$if MODE == "llm":
6367
${layout_declare_ubo(B, "int", "input_pos")}
6468
$if HAS_BIAS:
6569
${layout_declare_ubo(B, "ivec4", "bias_sizes")}
70+
$if HAS_MASK:
71+
${layout_declare_ubo(B, "ivec4", "mask_sizes")}
6672

6773
layout(local_size_x_id = 0, local_size_y_id = 1, local_size_z_id = 2) in;
6874

@@ -82,6 +88,9 @@ ${layout_declare_spec_const(C, "float", "inv_scale", "1.0")}
8288
* attn_weights: [B, H_q, S, context_len] in input dtype
8389
* current context_len = input_pos + S
8490
* Applies combined scale + causal mask.
91+
* With HAS_MASK the causal mask is replaced by an additive attn_mask
92+
* [S, context_len]; the caller passes input_pos = C - S so that every key
93+
* in k is attended, as in the CPU op.
8594
*
8695
* Fused SDPA:
8796
* q: [B, H, S, D] (DSHB layout)
@@ -164,7 +173,7 @@ void main() {
164173
const int attn_S = S;
165174
#endif
166175

167-
#ifdef HAS_INPUT_POS
176+
#if defined(HAS_INPUT_POS) && !defined(HAS_MASK)
168177
// If the tile is completely inside the mask region, then there is no need to
169178
// compute the output tile. All the elements in the output tile can be set to
170179
// negative infinity.
@@ -200,7 +209,11 @@ void main() {
200209
fp_accumulate_with_fp_weight(out_tile, q_tile, w_tile);
201210
}
202211

203-
#ifdef HAS_INPUT_POS
212+
#if defined(HAS_INPUT_POS) && defined(HAS_MASK)
213+
// LLM: scale + additive attn_mask
214+
apply_scale(out_tile, inv_scale);
215+
apply_attn_mask(out_tile, c, s);
216+
#elif defined(HAS_INPUT_POS)
204217
// LLM: combined scale + causal mask
205218
VEC4_T inv_scale_vec = VEC4_T(inv_scale);
206219
apply_scale_and_mask(

‎backends/vulkan/runtime/graph/ops/glsl/sdpa_compute_attn_weights_tiled.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ sdpa_compute_attn_weights_tiled:
1212
K_CACHE_STORAGE: texture3d
1313
MODE: llm
1414
HAS_BIAS: false
15+
HAS_MASK: false
1516
TILE_M4: 1
1617
TILE_K4: 1
1718
TILE_N4: 1
@@ -31,6 +32,8 @@ sdpa_compute_attn_weights_tiled:
3132
suffix: half
3233
shader_variants:
3334
- NAME: sdpa_compute_attn_weights_tiled
35+
- NAME: sdpa_compute_attn_weights_tiled_mask
36+
HAS_MASK: true
3437
- NAME: fused_sdpa_qk_tiled
3538
MODE: fused
3639
OUT_DTYPE: float

‎backends/vulkan/runtime/graph/ops/glsl/sdpa_fp_attn_weight_tile_store.glslh‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,42 @@ void apply_bias(
151151
}
152152
#endif // HAS_BIAS
153153

154+
#ifdef HAS_MASK
155+
/*
156+
* Adds the LLM SDPA additive attn_mask, a 2D [S, C] tensor shared by every
157+
* head, to the tile. Requires t_mask and mask_sizes. The mask is read element
158+
* by element: a buffer-backed mask is not padded along its width, so its rows
159+
* cannot be addressed in texels unless C is a multiple of 4.
160+
*/
161+
void apply_attn_mask(
162+
inout FPOutTile tile,
163+
const int c_idx_start,
164+
const int s_idx_start) {
165+
const int mask_C = mask_sizes.x;
166+
const int mask_S = mask_sizes.y;
167+
[[unroll]] for (int s = 0; s < TILE_M; ++s) {
168+
const int s_idx = s_idx_start + s;
169+
if (s_idx >= mask_S) {
170+
continue;
171+
}
172+
[[unroll]] for (int c4 = 0; c4 < TILE_N4; ++c4) {
173+
[[unroll]] for (int c4i = 0; c4i < 4; ++c4i) {
174+
const int c_idx = c_idx_start + mul_4(c4) + c4i;
175+
if (c_idx < mask_C) {
176+
#ifdef INPUT_BUFFER
177+
const float mask_val = float(t_mask[s_idx * mask_C + c_idx]);
178+
#else
179+
const float mask_val = float(texelFetch(
180+
t_mask, ivec3(div_4(c_idx), s_idx, 0), 0)[mod_4(c_idx)]);
181+
#endif
182+
tile.data[s][c4][c4i] += T(mask_val);
183+
}
184+
}
185+
}
186+
}
187+
}
188+
#endif // HAS_MASK
189+
154190
void set_out_tile_to_vec(out FPOutTile tile, const LINEAR_FP_OUTPUT_TILE_VEC4_T vec) {
155191
[[unroll]] for (int s = 0; s < TILE_M; ++s) {
156192
[[unroll]] for (int c4 = 0; c4 < TILE_N4; ++c4) { tile.data[s][c4] = vec; }

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

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,14 @@ vkapi::ShaderInfo pick_sdpa_qk_shader(
243243
const ValueRef q_projected = args.at(1).refs.at(0);
244244
const ValueRef k_cache = args.at(1).refs.at(1);
245245
const bool is_gemv = is_single_token(graph, q_projected);
246+
// An additive attn_mask is bound as a third input: q, k, attn_mask.
247+
const bool has_mask = args.at(1).refs.size() >= 3;
246248

247249
std::string shader_name = "sdpa_compute_attn_weights";
248250
shader_name += is_gemv ? "_coop" : "_tiled";
251+
if (has_mask) {
252+
shader_name += "_mask";
253+
}
249254
add_storage_type_suffix(shader_name, graph->storage_type_of(q_projected));
250255
add_storage_type_suffix(shader_name, graph->storage_type_of(k_cache));
251256
add_dtype_suffix(shader_name, graph->dtype_of(q_projected));
@@ -481,11 +486,12 @@ void add_sdpa_kv_cache_update_node(
481486
}
482487

483488
// Unified QK node (attn_weights = scale * Q @ K^T [+ bias]).
484-
// LLM: pass input_pos_symint (real symint), attn_mask = kDummyValueRef.
489+
// LLM: pass input_pos_symint (real symint), and attn_mask = a 2D [S, C]
490+
// additive mask that replaces the causal mask, or kDummyValueRef.
485491
// FUSED: pass input_pos_symint = kDummyValueRef, attn_mask = valid ref or
486492
// kDummyValueRef to indicate no bias. scale_val is always passed as
487-
// a spec const; the LLM path computes it per head_dim and FUSED may
488-
// inherit from the caller-supplied scale.
493+
// a spec const; both modes default it to 1/sqrt(head_dim) and use the
494+
// caller-supplied scale when there is one.
489495
void add_sdpa_compute_attn_weights_node(
490496
ComputeGraph& graph,
491497
const ValueRef q,
@@ -503,7 +509,8 @@ void add_sdpa_compute_attn_weights_node(
503509

504510
if (mode == SDPAMode::LLM) {
505511
param_ubos.append(graph.get_or_create_int_param_buffer(input_pos_symint));
506-
} else if (is_valid(attn_mask)) {
512+
}
513+
if (is_valid(attn_mask)) {
507514
param_ubos.append(graph.sizes_ubo(attn_mask));
508515
read_inputs.push_back(attn_mask);
509516
}
@@ -640,6 +647,33 @@ void add_sdpa_compute_out_node(
640647
resize_sdpa_out_node));
641648
}
642649

650+
// For a masked LLM SDPA, sets out_symint = C - S (the number of keys in k
651+
// minus the number of queries), which the SDPA shaders read in place of
652+
// input_pos so that they attend to every key.
653+
void resize_attn_mask_input_pos_node(
654+
ComputeGraph* graph,
655+
const std::vector<ArgGroup>& args,
656+
const std::vector<ValueRef>& resize_args) {
657+
(void)args;
658+
const ValueRef q = resize_args.at(0);
659+
const ValueRef k = resize_args.at(1);
660+
const ValueRef out_symint = resize_args.at(2);
661+
graph->set_symint(
662+
out_symint,
663+
graph->size_at<int32_t>(-3, k) - graph->size_at<int32_t>(-3, q));
664+
}
665+
666+
ValueRef add_attn_mask_input_pos_node(
667+
ComputeGraph& graph,
668+
const ValueRef q,
669+
const ValueRef k) {
670+
const ValueRef out_symint = graph.add_symint(
671+
graph.size_at<int32_t>(-3, k) - graph.size_at<int32_t>(-3, q));
672+
graph.execute_nodes().emplace_back(
673+
new ExecuteNode(resize_attn_mask_input_pos_node, {q, k, out_symint}));
674+
return out_symint;
675+
}
676+
643677
//
644678
// High level operator impl
645679
//
@@ -696,11 +730,30 @@ void sdpa_impl(ComputeGraph& graph, const std::vector<ValueRef>& args) {
696730
VK_CHECK_COND(
697731
graph.val_is_none(dropout_p) ||
698732
graph.extract_scalar<double>(dropout_p) == 0);
699-
VK_CHECK_COND(graph.val_is_none(scale));
700-
// is_causal is assumed to be true in the current implementation.
701-
VK_CHECK_COND(
702-
graph.val_is_none(is_causal) || graph.extract_scalar<bool>(is_causal));
703-
VK_CHECK_COND(graph.val_is_none(attn_mask));
733+
734+
// As in the CPU op, an attn_mask replaces the causal mask: it is a 2D
735+
// [S, C] additive mask over every key in k_cache, and cannot be combined
736+
// with is_causal. Without one, the attention is assumed to be causal.
737+
const bool has_mask = graph.val_is_not_none(attn_mask);
738+
if (has_mask) {
739+
VK_CHECK_COND(
740+
graph.val_is_none(is_causal) || !graph.extract_scalar<bool>(is_causal),
741+
"attn_mask and is_causal cannot both be set");
742+
VK_CHECK_COND(graph.dim_of(attn_mask) == 2);
743+
VK_CHECK_COND(
744+
graph.size_at<int64_t>(-2, attn_mask) ==
745+
graph.size_at<int64_t>(-3, q_projected));
746+
VK_CHECK_COND(
747+
graph.size_at<int64_t>(-1, attn_mask) ==
748+
graph.size_at<int64_t>(-3, k_cache));
749+
VK_CHECK_COND(graph.dtype_of(attn_mask) == graph.dtype_of(q_projected));
750+
VK_CHECK_COND(
751+
graph.storage_type_of(attn_mask) == graph.storage_type_of(q_projected));
752+
VK_CHECK_COND(graph.packed_dim_of(attn_mask) == WHCN::kWidthDim);
753+
} else {
754+
VK_CHECK_COND(
755+
graph.val_is_none(is_causal) || graph.extract_scalar<bool>(is_causal));
756+
}
704757

705758
const int64_t num_q_heads = graph.size_at<int64_t>(-2, q_projected);
706759
int64_t max_seq_len = graph.size_at<int64_t>(-3, q_projected);
@@ -763,14 +816,23 @@ void sdpa_impl(ComputeGraph& graph, const std::vector<ValueRef>& args) {
763816
utils::kWidthPacked);
764817

765818
const int32_t head_dim_size = graph.size_at<int32_t>(-1, q_projected);
766-
const float scale_val = 1.0f / std::sqrt(static_cast<float>(head_dim_size));
819+
const float scale_val = graph.val_is_none(scale)
820+
? 1.0f / std::sqrt(static_cast<float>(head_dim_size))
821+
: graph.extract_scalar<float>(scale);
822+
823+
// The SDPA shaders attend to the first input_pos + S keys. A masked
824+
// attention attends to all C keys in k_cache, so give them C - S in place of
825+
// input_pos.
826+
const ValueRef attn_input_pos = has_mask
827+
? add_attn_mask_input_pos_node(graph, q_projected, k_cache)
828+
: input_pos_symint;
767829

768830
add_sdpa_compute_attn_weights_node(
769831
graph,
770832
q_projected,
771833
k_cache,
772-
input_pos_symint,
773-
/*attn_mask=*/kDummyValueRef,
834+
attn_input_pos,
835+
has_mask ? attn_mask : kDummyValueRef,
774836
scale_val,
775837
attn_weights,
776838
SDPAMode::LLM);
@@ -780,7 +842,7 @@ void sdpa_impl(ComputeGraph& graph, const std::vector<ValueRef>& args) {
780842
attn_weights,
781843
q_projected,
782844
k_cache,
783-
input_pos_symint,
845+
attn_input_pos,
784846
attn_weights_softmax,
785847
SDPAMode::LLM);
786848

@@ -790,7 +852,7 @@ void sdpa_impl(ComputeGraph& graph, const std::vector<ValueRef>& args) {
790852
v_cache,
791853
q_projected,
792854
/*k=*/kDummyValueRef,
793-
input_pos_symint,
855+
attn_input_pos,
794856
out,
795857
SDPAMode::LLM);
796858
}

0 commit comments

Comments
 (0)