Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 76 additions & 8 deletions ggml/src/ggml-openvino/ggml-decoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,32 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const {
is_kvcache(node->src[1]->view_src, nullptr)) {
// s_copy defrag remainder writeback: gathered extra state rows copied back into the cache
op_case = 3;
} else if (node->src[1] != nullptr && node->src[1]->op == GGML_OP_VIEW &&
node->src[1]->view_src != nullptr) {
// op_case 5: KV write for decoder self-attention (dynamic write offset)
// op_case 6: KV write for encoder self-attn or cross-attn (static offset)
const ggml_tensor * kv_buf = node->src[1]->view_src;
if (kv_buf->ne[1] == 1 && kv_buf->ne[2] == 1 && kv_buf->ne[3] == 1) {
op_case = 6;
// Forward-scan the graph for a FLASH_ATTN_EXT that reads from
// the same buffer. Having a mask (src[3] != nullptr) implies
// decoder self-attention and the write offset is dynamic.
for (int i = 0; i < m_cgraph->n_nodes; i++) {
const ggml_tensor * n = m_cgraph->nodes[i];
if (n->op != GGML_OP_FLASH_ATTN_EXT) {
continue;
}
// K (src[1]) and V (src[2]) are 3-D views whose view_src is
// the flat KV buffer we are writing to.
if ((n->src[1] != nullptr && n->src[1]->view_src == kv_buf) ||
(n->src[2] != nullptr && n->src[2]->view_src == kv_buf)) {
if (n->src[3] != nullptr) {
op_case = 5; // decoder self-attention: mask present
}
break;
}
}
}
}
break;
}
Expand All @@ -380,6 +406,16 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const {
op_case = 1;
}
break;
}
case GGML_OP_FLASH_ATTN_EXT: {
if (node->src[1] != nullptr && node->src[1]->op == GGML_OP_VIEW &&
node->src[1]->view_src != nullptr) {
const ggml_tensor * kv_buf = node->src[1]->view_src;
if (kv_buf->ne[1] == 1 && kv_buf->ne[2] == 1 && kv_buf->ne[3] == 1) {
op_case = (node->src[3] != nullptr) ? 1 : 2;
}
}
break;
}
default:
break;
Expand Down Expand Up @@ -412,23 +448,36 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr

switch (node->op) {
case GGML_OP_FLASH_ATTN_EXT:
if (node->src[0] == nullptr || node->src[1] == nullptr || node->src[3] == nullptr) {
if (node->src[0] == nullptr || node->src[1] == nullptr) {
return -1;
}
switch (node->src[1]->op) {
case GGML_OP_PERMUTE:
// case 0: node op is FLASH_ATTN_EXT, src 1 not null & op is PERMUTE & the permuted tensor src is the view of cache k
if (node->src[1]->src[0] != nullptr && node->src[1]->src[0]->op == GGML_OP_VIEW) {
// case 0: src[1] is PERMUTE of a cache VIEW, mask required
if (node->src[3] != nullptr && node->src[1]->src[0] != nullptr &&
node->src[1]->src[0]->op == GGML_OP_VIEW) {
return 0;
}
break;
case GGML_OP_CPY:
// case 1: node op is FLASH_ATTN_EXT, src 1 not null & op is CPY & the copied tensor src is PERMUTE & the permuted tensor src is the view of cache k
if (node->src[1]->src[0] != nullptr && node->src[1]->src[0]->op == GGML_OP_PERMUTE &&
node->src[1]->src[0]->src[0] != nullptr && node->src[1]->src[0]->src[0]->op == GGML_OP_VIEW) {
// case 1: src[1] is CPY of a PERMUTE(VIEW), mask required
if (node->src[3] != nullptr && node->src[1]->src[0] != nullptr &&
node->src[1]->src[0]->op == GGML_OP_PERMUTE &&
node->src[1]->src[0]->src[0] != nullptr &&
node->src[1]->src[0]->src[0]->op == GGML_OP_VIEW) {
return 1;
}
break;
case GGML_OP_VIEW:
// cases 4/5/6: whisper - K is a direct non-contiguous VIEW_3D of a KV cache
if (node->src[1]->view_src != nullptr) {
if (node->src[3] != nullptr) {
return 4; // decoder self-attention
} else {
return 5; // cross-attention or encoder self-attention
};
}
break;
default:
break;
}
Expand Down Expand Up @@ -481,6 +530,18 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
cache_k_permute = node->src[0]->src[0]->src[0];
mask = node->src[1];
break;
case 4:
case 5: {
// whisper: K is a direct VIEW_3D of the KV buffer, no PERMUTE node
auto * cache_k_view = node->src[1]; // VIEW_3D of kv_self.k or kv_cross.k`
compute_params.token_len_per_seq = node->src[0]->ne[1];
if (attention_pattern_case == 4) {
compute_params.attention_size = cache_k_view->ne[1];
} else {
compute_params.attention_size_static = cache_k_view->ne[1];
}
continue;
}
default:
break;
}
Expand Down Expand Up @@ -634,11 +695,15 @@ ov::PartialShape GgmlOvDecoder::get_graph_input_shape(const ggml_tensor * op,
} else if (is_kvcache(input, op)) {
// kvcache
input_shape = ov::PartialShape{get_shape(input)};
if (!m_is_static) {
// Whisper.cpp uses a fixed size 1D KV buffer [N, 1, 1, 1] (GGML) or [1, 1, 1, N] (OV).
// the token fill level is handled by token_len_per_seq + dynamic mask input.
// skip dynamic dim and stateful reshape for this layout.
const bool is_flat_kv = (input->ne[1] == 1 && input->ne[2] == 1 && input->ne[3] == 1);
if (!m_is_static && !is_flat_kv) {
// do not fix ctx size to make llama-bench work across test params
input_shape[2] = -1;
}
if (is_stateful()) {
if (is_stateful() && !is_flat_kv) {
// Convert stateless KV cache layout [1, 1, seq, n_heads_kv * head_size]
// to stateful layout [1, seq, n_heads_kv, head_size].
assert(input_shape.size() == 4 && input_shape[0] == 1 && input_shape[1] == 1 &&
Expand Down Expand Up @@ -713,6 +778,9 @@ void GgmlOvDecoder::add_extra_inputs() {
if (m_compute_params.attention_size != -1) {
create_1d_input("attention_size", m_compute_params.attention_size);
}
if (m_compute_params.attention_size_static != -1) {
create_1d_input("attention_size_static", m_compute_params.attention_size_static);
}
if (m_compute_params.attention_size_swa != -1) {
create_1d_input("attention_size_swa", m_compute_params.attention_size_swa);
}
Expand Down
1 change: 1 addition & 0 deletions ggml/src/ggml-openvino/ggml-decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ struct ComputeParams {
int seq_active_start = 0;
int attention_size = -1;
int attention_size_swa = -1;
int attention_size_static = -1; // encoder/cross-attn KV fill level (whisper)
int input_len = -1;
int token_len_per_seq = -1;
int past_kv_len = -1;
Expand Down
22 changes: 19 additions & 3 deletions ggml/src/ggml-openvino/ggml-openvino.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -809,11 +809,27 @@ static bool has_non_contiguous_view_input(const ggml_tensor * op) {
}

static bool is_supported_flash_attn_pattern(const ggml_tensor * op) {
// pattern of q,k,v should be q->op==PERMUTE, q->src[0]->op==VIEW, q->src[0]->src[0]->view_src==nullptr
// Each Q/K/V input must follow one of:
// PERMUTE -> VIEW -> base (view_src==nullptr) (llama KV-cache path)
// PERMUTE -> RESHAPE -> base (view_src==nullptr) (whisper Q)
// VIEW -> base (view_src==nullptr) (whisper K/V from kv_pad)
for (int i = 0; i < 3; i++) {
const ggml_tensor * src = op->src[i];
if (src->op != GGML_OP_PERMUTE || src->src[0] == nullptr || src->src[0]->op != GGML_OP_VIEW ||
src->src[0]->src[0] == nullptr || src->src[0]->src[0]->view_src != nullptr) {
if (src->op == GGML_OP_PERMUTE) {
if (src->src[0] == nullptr) {
return false;
}
if (src->src[0]->op != GGML_OP_VIEW && src->src[0]->op != GGML_OP_RESHAPE) {
return false;
}
if (src->src[0]->src[0] == nullptr || src->src[0]->src[0]->view_src != nullptr) {
return false;
}
} else if (src->op == GGML_OP_VIEW) {
if (src->src[0] == nullptr || src->src[0]->view_src != nullptr) {
return false;
}
} else {
return false;
}
}
Expand Down
69 changes: 67 additions & 2 deletions ggml/src/ggml-openvino/openvino/op/cpy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
#include "../utils.h"

#include <climits>
#include <cstdint>
#include <cstdio>
#include <memory>
#include <openvino/frontend/exception.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/range.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/negative.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/scatter_update.hpp>
#include <openvino/op/subtract.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>

Expand All @@ -22,6 +29,7 @@ namespace op {

OutputVector translate_cpy(const NodeContext & context) {
auto op_case = context.get_op_case();
auto input = process_view_input_new(context, 0);
auto input_shape = context.get_input_shape(0);
auto output_shape = context.get_input_shape(1);

Expand Down Expand Up @@ -121,7 +129,64 @@ OutputVector translate_cpy(const NodeContext & context) {
return rename_outputs_with_suffix({res}, context.get_name());
}

auto input = process_view_input_new(context, 0);
if (op_case == 5 || op_case == 6) {
auto input_shape = context.get_input_shape(0);
auto output_shape = context.get_output_shape();
auto dst_ggml_shape = context.get_view_input_ggml_shape(1, 0);
auto dst_stride = context.get_view_input_stride(1, 0);
size_t offset_bytes = context.get_view_input_offset(1, 0);
auto n_state = (int64_t)context.get_input_shape(0)[3].get_length();
auto n_state_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_state});
auto kv_buf = context.get_input(1); // shape {1,1,1,N}

Output<Node> token_len_per_seq;
Output<Node> n_write_dyn;
if (context.has_input("token_len_per_seq")){
token_len_per_seq = context.get_input("token_len_per_seq");
n_write_dyn = std::make_shared<ov::op::v1::Multiply>(token_len_per_seq, n_state_c);
} else {
n_write_dyn = ov::op::v0::Constant::create(ov::element::i64, {1}, {(int64_t)dst_ggml_shape[3]});
}
size_t elem_size = dst_stride[3];
FRONT_END_OP_CONVERSION_CHECK(elem_size > 0, "CPY KV cache view update has invalid element size");
int64_t start_elem = (int64_t)(offset_bytes / elem_size);
// op_case 5: decoder self-attention – write offset advances each step.
// op_case 6: encoder self-attn or cross-attn – offset fixed at compile time.
const bool is_decoder_self_attn = (op_case == 5);
auto ones_c = ov::op::v0::Constant::create(ov::element::i64, {3}, std::vector<int64_t>{1, 1, 1});
auto new_shape = std::make_shared<ov::op::v0::Concat>(
ov::OutputVector{ones_c, n_write_dyn}, 0);

auto reshaped = std::make_shared<ov::op::v1::Reshape>(input, new_shape, false);
auto data = std::make_shared<ov::op::v0::Convert>(
reshaped,
context.get_output_type());
// Indices [start_elem .. start_elem + n_write) on axis 3 of {1,1,1,N}
// For decoder self-attention the write offset advances each step, so compute it
// dynamically from the model inputs: start = (attention_size - token_len_per_seq) * n_state.
// For encoder self-attn and cross-attn the offset is fixed at graph-compile time.
ov::Output<ov::Node> start;
if (is_decoder_self_attn &&
context.has_input("attention_size") && context.has_input("token_len_per_seq")) {
auto attention_size_in = context.get_input("attention_size");
auto token_len_in = context.get_input("token_len_per_seq");
auto past_tokens = std::make_shared<ov::op::v1::Subtract>(attention_size_in, token_len_in);
auto new_start = std::make_shared<ov::op::v1::Multiply>(past_tokens, n_state_c);
start = std::make_shared<ov::op::v1::Add>(new_start, ov::op::v0::Constant::create(ov::element::i64, {1}, {start_elem}));
} else {
start = ov::op::v0::Constant::create(ov::element::i64, {1}, {start_elem});
}
auto start_squeezed = std::make_shared<ov::op::v0::Squeeze>(start);
auto end = std::make_shared<ov::op::v1::Add>(start_squeezed, n_write_dyn);
auto end_squeezed = std::make_shared<ov::op::v0::Squeeze>(end);
auto step = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto step_squeezed = std::make_shared<ov::op::v0::Squeeze>(step);
auto indices = std::make_shared<ov::op::v4::Range>(start_squeezed, end_squeezed, step_squeezed, ov::element::i64);
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});

auto kv_updated = std::make_shared<ov::op::v3::ScatterUpdate>(kv_buf, indices, data, axis);
return rename_outputs_with_suffix({kv_updated}, context.get_name());
}

if (input_shape != output_shape) {
auto new_shape = ov::op::v0::Constant::create(
Expand Down
Loading
Loading