Skip to content

Commit a39bba2

Browse files
committed
ggml-openvino: support gemma-2 soft-capped attention
Gemma-2 caps its attention scores as softcap * tanh(scores / softcap) before the softmax. FLASH_ATTN_EXT now applies the cap on the manual attention path, as SDPA has no soft-capping, and the decoder looks through the SCALE, TANH, SCALE chain of -fa off when it matches attention. One helper finds the K cache of an attention for both the sliding-window pre-pass and the main pass. The pre-pass copy did not see through the chain, so the sliding-window layers used the full-attention sizes once the context exceeded n_swa + n_ubatch. Soft-capped attention keeps the flat KV cache layout: with the 4-D layout the GPU plugin fails in its oneDNN path.
1 parent 25e3135 commit a39bba2

5 files changed

Lines changed: 73 additions & 31 deletions

File tree

‎ggml/src/ggml-openvino/ggml-decoder.cpp‎

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,16 @@ static int get_swa_window_from_mask(const ggml_tensor * mask) {
762762
return window > 0 ? (int) window : -1;
763763
}
764764

765+
// The KQ product that the SOFT_MAX of an attention reads: directly, or through the logit soft-capping of gemma-2
766+
// (SCALE, TANH, SCALE)
767+
static const ggml_tensor * softmax_scores_source(const ggml_tensor * scores) {
768+
while (scores != nullptr && (scores->op == GGML_OP_SCALE ||
769+
(scores->op == GGML_OP_UNARY && ggml_get_unary_op(scores) == GGML_UNARY_OP_TANH))) {
770+
scores = scores->src[0];
771+
}
772+
return scores;
773+
}
774+
765775
std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgraph * cgraph, bool is_static) {
766776
ModelParams model_params;
767777
ComputeParams compute_params;
@@ -804,12 +814,12 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
804814
break;
805815
}
806816
break;
807-
case GGML_OP_SOFT_MAX:
808-
// case 2: node op is SOFT_MAX, src 0 not null & op is MUL_MAT & the src 0 of MUL_MAT is PERMUTE & the permuted tensor src is the view of cache k
809-
if (node->src[0] != nullptr && node->src[1] != nullptr && node->src[0]->op == GGML_OP_MUL_MAT &&
810-
node->src[0]->src[0] != nullptr && node->src[0]->src[1] != nullptr &&
811-
node->src[0]->src[0]->op == GGML_OP_PERMUTE && node->src[0]->src[0]->src[0] != nullptr &&
812-
node->src[0]->src[0]->src[0]->op == GGML_OP_VIEW) {
817+
case GGML_OP_SOFT_MAX: {
818+
// case 2: node op is SOFT_MAX, its scores come from a MUL_MAT (see softmax_scores_source) & the src 0 of MUL_MAT is PERMUTE & the permuted tensor src is the view of cache k
819+
const ggml_tensor * kq = softmax_scores_source(node->src[0]);
820+
if (kq != nullptr && node->src[1] != nullptr && kq->op == GGML_OP_MUL_MAT && kq->src[0] != nullptr &&
821+
kq->src[1] != nullptr && kq->src[0]->op == GGML_OP_PERMUTE && kq->src[0]->src[0] != nullptr &&
822+
kq->src[0]->src[0]->op == GGML_OP_VIEW) {
813823
return 2;
814824
}
815825
// case 3: node op is SOFT_MAX, src 0 not null & op is ADD & the src 0 of ADD is MUL_MAT & the src 0 of MUL_MAT is PERMUTE
@@ -819,6 +829,7 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
819829
return 3;
820830
}
821831
break;
832+
}
822833
default:
823834
break;
824835
}
@@ -841,6 +852,22 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
841852
}
842853
};
843854

855+
// The PERMUTE of the K cache view an attention node reads, for the same src layouts
856+
auto get_attention_cache_k_permute = [&get_attention_pattern_case](const ggml_tensor * node) -> ggml_tensor * {
857+
switch (get_attention_pattern_case(node)) {
858+
case 0:
859+
return node->src[1];
860+
case 1:
861+
return node->src[1]->src[0];
862+
case 2:
863+
return softmax_scores_source(node->src[0])->src[0];
864+
case 3:
865+
return node->src[0]->src[0]->src[0];
866+
default:
867+
return nullptr;
868+
}
869+
};
870+
844871
// Pre-pass: classify sliding-window vs full-attention layers.
845872
//
846873
// An interleaved-SWA model keeps two KV caches and two attention masks, and hands each layer
@@ -874,14 +901,7 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
874901
if (mask == nullptr) {
875902
continue;
876903
}
877-
const ggml_tensor * cache_k_permute = nullptr;
878-
switch (get_attention_pattern_case(cgraph->nodes[i])) {
879-
case 0: cache_k_permute = cgraph->nodes[i]->src[1]; break;
880-
case 1: cache_k_permute = cgraph->nodes[i]->src[1]->src[0]; break;
881-
case 2: cache_k_permute = cgraph->nodes[i]->src[0]->src[0]; break;
882-
default: cache_k_permute = cgraph->nodes[i]->src[0]->src[0]->src[0]; break;
883-
}
884-
const ggml_tensor * cache_k_view = cache_k_permute->src[0];
904+
const ggml_tensor * cache_k_view = get_attention_cache_k_permute(cgraph->nodes[i])->src[0];
885905
if (cache_k_view->op != GGML_OP_VIEW) {
886906
continue;
887907
}
@@ -950,26 +970,23 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
950970
node->src[2]->ne[0] == 1 && is_kvcache(node->src[2]->src[0], nullptr)) {
951971
model_params.v_trans = true;
952972
}
973+
if (node->op == GGML_OP_FLASH_ATTN_EXT) {
974+
float logit_softcap = 0.0f;
975+
memcpy(&logit_softcap, (const float *) node->op_params + 2, sizeof(float));
976+
model_params.softcap_attn = model_params.softcap_attn || logit_softcap != 0.0f;
977+
}
953978
const int attention_pattern_case = get_attention_pattern_case(node);
954979
if (attention_pattern_case != -1) {
955-
ggml_tensor * cache_k_permute = nullptr;
980+
ggml_tensor * cache_k_permute = get_attention_cache_k_permute(node);
956981
ggml_tensor * mask = nullptr;
957982

958983
switch (attention_pattern_case) {
959984
case 0:
960-
cache_k_permute = node->src[1];
961-
mask = node->src[3];
962-
break;
963985
case 1:
964-
cache_k_permute = node->src[1]->src[0];
965986
mask = node->src[3];
966987
break;
967988
case 2:
968-
cache_k_permute = node->src[0]->src[0];
969-
mask = node->src[1];
970-
break;
971989
case 3:
972-
cache_k_permute = node->src[0]->src[0]->src[0];
973990
mask = node->src[1];
974991
break;
975992
case 4:
@@ -1593,8 +1610,9 @@ bool GgmlOvDecoder::is_kvcache_4d(const ggml_tensor * cache) const {
15931610
return false;
15941611
}
15951612
// `-fa off` (transposed V cache): keep the [1, 1, ctx, n_heads_kv * head_size] layout. The 4-D K cache breaks the
1596-
// MUL_MAT attention of the Phi-3 models (Phi-3.5 mini, Phi-4 mini).
1597-
if (m_model_params.v_trans) {
1613+
// MUL_MAT attention of the Phi-3 models (Phi-3.5 mini, Phi-4 mini), and the soft-capped manual attention of
1614+
// gemma-2 with `-fa on` ("could not construct a memory descriptor using strides" in the GPU plugin's oneDNN path).
1615+
if (m_model_params.v_trans || m_model_params.softcap_attn) {
15981616
return false;
15991617
}
16001618
if (cache->op != GGML_OP_NONE || cache->view_src != nullptr || !is_kvcache(cache, nullptr)) {

‎ggml/src/ggml-openvino/ggml-decoder.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ struct ModelParams {
3737
bool is_cacheless_attn = false;
3838
// the graph writes a transposed V cache (`-fa off`: one value element per row, tokens along the row)
3939
bool v_trans = false;
40+
// the graph has FLASH_ATTN_EXT with logit soft-capping (gemma-2)
41+
bool softcap_attn = false;
4042
std::vector<int> swa_layers;
4143
// The sliding-window mask tensor, identified in compute_llm_params() by grouping attention
4244
// layers on the mask they consume. Only used to tell the two masks apart when naming OV

‎ggml/src/ggml-openvino/ggml-openvino.cpp‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,8 +1646,10 @@ static ggml_openvino_op_support is_op_supported_case(const ggml_tensor * op) {
16461646
if (max_bias > 0) {
16471647
return {false, "FLASH_ATTN_EXT with max_bias > 0 (max_bias=" + std::to_string(max_bias) + ") is not supported"};
16481648
}
1649-
if (logit_softcap != 0) {
1650-
return {false, "FLASH_ATTN_EXT with logit_softcap != 0 (logit_softcap=" + std::to_string(logit_softcap) + ") is not supported"};
1649+
// soft-capping (gemma-2) is applied on the manual attention path of translate_flash_attn_ext, which needs one
1650+
// head size for Q, K and V
1651+
if (logit_softcap != 0 && (op->src[1]->ne[0] != op->src[0]->ne[0] || op->src[2]->ne[0] != op->src[0]->ne[0])) {
1652+
return {false, "FLASH_ATTN_EXT with logit_softcap != 0 needs one head size for Q, K and V"};
16511653
}
16521654
break;
16531655
}

‎ggml/src/ggml-openvino/openvino/op/flash_attn_ext.cpp‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <openvino/op/scatter_update.hpp>
2020
#include <openvino/op/slice.hpp>
2121
#include <openvino/op/softmax.hpp>
22+
#include <openvino/op/tanh.hpp>
2223
#include <openvino/op/tile.hpp>
2324
#include <openvino/op/transpose.hpp>
2425
#include <openvino/op/unsqueeze.hpp>
@@ -136,7 +137,7 @@ OutputVector translate_flash_attn_ext(const NodeContext & context) {
136137
float * params = reinterpret_cast<float *>(context.get_output_op_params());
137138
float scale = params[0];
138139
// float max_bias = params[1];
139-
// float logit_softcap = params[2];
140+
const float logit_softcap = params[2];
140141

141142
auto q = std::make_shared<ov::op::v0::Convert>(q_f32, ov::element::f16);
142143
auto scale_node = std::make_shared<ov::op::v0::Constant>(ov::element::f16, ov::Shape{}, std::vector<float>{scale});
@@ -183,8 +184,14 @@ OutputVector translate_flash_attn_ext(const NodeContext & context) {
183184
// The manual path reshapes Q, K, V and the output with one head size; models whose K/V head sizes differ
184185
// from Q's (e.g. MLA) take the SDPA path.
185186
const bool same_head_size = k_shape[3] == q_shape[3] && v_shape[3] == q_shape[3];
187+
// SDPA has no logit soft-capping (gemma-2), so soft-capped attention always takes the manual path, also in
188+
// stateful execution
186189
const bool use_manual_gqa_attention =
187-
manual_gqa_enabled && factor > 1 && num_heads_kv > 1 && same_head_size && !context.is_stateful();
190+
(logit_softcap != 0.0f || (manual_gqa_enabled && factor > 1 && num_heads_kv > 1 && !context.is_stateful())) &&
191+
same_head_size;
192+
FRONT_END_OP_CONVERSION_CHECK(logit_softcap == 0.0f || use_manual_gqa_attention,
193+
"FLASH_ATTN_EXT with logit soft-capping is not supported with different K/V head "
194+
"sizes");
188195

189196
if (use_manual_gqa_attention) {
190197
// Q, K, V arrive as [B, n_heads(_kv), S, head_size], where B is the active
@@ -205,7 +212,18 @@ OutputVector translate_flash_attn_ext(const NodeContext & context) {
205212

206213
// QK^T -> [B, num_heads_kv, factor * S_q, S_k]
207214
auto qk = std::make_shared<ov::op::v0::MatMul>(q_r, k, /*tA=*/false, /*tB=*/true);
208-
ov::Output<ov::Node> probs = std::make_shared<ov::op::v1::Multiply>(qk, scale_node);
215+
ov::Output<ov::Node> probs;
216+
if (logit_softcap != 0.0f) {
217+
// as ggml: softcap * tanh(qk * scale / softcap)
218+
probs = std::make_shared<ov::op::v1::Multiply>(
219+
qk,
220+
ov::op::v0::Constant::create(ov::element::f16, ov::Shape{}, std::vector<float>{scale / logit_softcap}));
221+
probs = std::make_shared<ov::op::v1::Multiply>(
222+
std::make_shared<ov::op::v0::Tanh>(probs),
223+
ov::op::v0::Constant::create(ov::element::f16, ov::Shape{}, std::vector<float>{logit_softcap}));
224+
} else {
225+
probs = std::make_shared<ov::op::v1::Multiply>(qk, scale_node);
226+
}
209227

210228
if (has_mask) {
211229
// Mask arrives as [B, 1, S_q, S_k]. Row j * S_q + s of the scores belongs to query position s, so

‎ggml/src/ggml-openvino/openvino/op/permute.cpp‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ OutputVector translate_permute(const NodeContext & context) {
121121

122122
Output<Node> attention_size;
123123
if (!context.has_input("attention_size")) {
124-
attention_size = ov::op::v0::Constant::create(ov::element::i64, {1}, {output_shape[2]});
124+
// the attended tokens are the last axis of the transposed V cache (`-fa off`)
125+
const size_t tokens_axis = (op_case == 5 || op_case == 6) ? 3 : 2;
126+
attention_size = ov::op::v0::Constant::create(ov::element::i64, {1}, {output_shape[tokens_axis]});
125127
} else if (op_case == 3 || op_case == 5) {
126128
attention_size = context.get_input("attention_size");
127129
} else {

0 commit comments

Comments
 (0)