[GPU] KVCache custom op and fusion pass (#21689)

This commit is contained in:
Vladimir Paramuzov authored and GitHub committed 2023-12-19 17:56:25 +04:00
1 parent 6719133af2
commit 294cc6668c
23 files changed
+1319 -4

No files matched your search

@@ -0,0 +1,64 @@
// Copyright (C) 2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#pragma once
#include "openvino/op/op.hpp"
#include "openvino/op/util/variable.hpp"
#include "openvino/op/util/variable_extension.hpp"
namespace ov {
namespace intel_gpu {
namespace op {
/// \brief Operator that implements Key-Values cache subgraph for large language models.
/// This operation updates data of the corresponding Variable
class KVCache : public ov::op::Op, public ov::op::util::VariableExtension {
public:
OPENVINO_OP("KVCache", "gpu_opset");
KVCache() = default;
KVCache(const Output<Node>& past,
const Output<Node>& new_token_data,
const Output<Node>& beam_idx,
const std::shared_ptr<ov::op::util::Variable>& past_values,
int64_t concat_axis,
int64_t gather_axis,
const ov::element::Type output_type = ov::element::undefined);
KVCache(const Output<Node>& past,
const Output<Node>& new_token_data,
const std::shared_ptr<ov::op::util::Variable>& past_values,
int64_t concat_axis,
const ov::element::Type output_type = ov::element::undefined);
bool visit_attributes(ov::AttributeVisitor& visitor) override;
void validate_and_infer_types() override;
std::shared_ptr<Node> clone_with_new_inputs(const ov::OutputVector& new_args) const override;
std::string get_variable_id() const override {
OPENVINO_ASSERT(m_variable, "Variable is not initialized. Variable_id is unavailable");
return m_variable->get_info().variable_id;
}
int64_t get_concat_axis() const { return m_concat_axis; }
int64_t get_gather_axis() const { return m_gather_axis; }
void set_concat_axis(int64_t axis) { m_concat_axis = axis; }
void set_gather_axis(int64_t axis) { m_gather_axis = axis; }
private:
int64_t m_concat_axis;
int64_t m_gather_axis;
ov::element::Type m_output_type;
};
std::vector<ov::PartialShape> shape_infer(const KVCache* op, std::vector<ov::PartialShape> input_shapes);
} // namespace op
} // namespace intel_gpu
} // namespace ov
@@ -0,0 +1,40 @@
// Copyright (C) 2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#pragma once
#include "openvino/op/op.hpp"
#include "openvino/op/util/variable.hpp"
#include "openvino/op/util/variable_extension.hpp"
namespace ov {
namespace intel_gpu {
namespace op {
/// \brief Similar to common v6::ReadValue, but it's not derived from ReadValueBase class to avoid ReadValue-Assign pairing check
/// This is needed to have ReadValue-KVCache pair instead of ReadValue-Assign
class ReadValue : public ov::op::Op, public ov::op::util::VariableExtension {
public:
OPENVINO_OP("ReadValue", "gpu_opset");
ReadValue() = default;
ReadValue(const std::shared_ptr<ov::op::util::Variable>& variable);
ReadValue(const Output<Node>& variable_initializer, const std::shared_ptr<ov::op::util::Variable>& variable);
bool visit_attributes(ov::AttributeVisitor& visitor) override;
void validate_and_infer_types() override;
std::shared_ptr<Node> clone_with_new_inputs(const ov::OutputVector& new_args) const override;
std::string get_variable_id() const override {
OPENVINO_ASSERT(m_variable, "Variable is not initialized. Variable_id is unavailable");
return m_variable->get_info().variable_id;
}
};
} // namespace op
} // namespace intel_gpu
} // namespace ov
@@ -273,3 +273,5 @@ REGISTER_FACTORY(internal, FullyConnected);
REGISTER_FACTORY(internal, FullyConnectedCompressed);
REGISTER_FACTORY(internal, RMS);
REGISTER_FACTORY(internal, GatherCompressed);
REGISTER_FACTORY(internal, KVCache);
REGISTER_FACTORY(internal, ReadValue);
@@ -0,0 +1,72 @@
// Copyright (C) 2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#pragma once
#include "openvino/core/partial_shape.hpp"
#include "openvino/core/type/element_type.hpp"
#include "openvino/op/util/variable.hpp"
#include "primitive.hpp"
#include <vector>
namespace cldnn {
struct kv_cache : public primitive_base<kv_cache> {
CLDNN_DECLARE_PRIMITIVE(kv_cache)
kv_cache() : primitive_base("", {}) {}
kv_cache(const primitive_id& id,
const std::vector<input_info>& inputs,
const ov::op::util::VariableInfo& variable_info,
const int64_t concat_axis,
const int64_t gather_axis,
const padding& output_padding = padding())
: primitive_base(id, inputs, {output_padding}), variable_info(variable_info), concat_axis(concat_axis), gather_axis(gather_axis) {}
ov::op::util::VariableInfo variable_info;
int64_t concat_axis = 0;
int64_t gather_axis = 0;
size_t hash() const override {
size_t seed = primitive::hash();
seed = hash_combine(seed, concat_axis);
seed = hash_combine(seed, gather_axis);
return seed;
}
bool operator==(const primitive& rhs) const override {
if (!compare_common_params(rhs))
return false;
auto rhs_casted = downcast<const kv_cache>(rhs);
return variable_info == rhs_casted.variable_info &&
concat_axis == rhs_casted.concat_axis &&
gather_axis == rhs_casted.gather_axis;
}
void save(BinaryOutputBuffer& ob) const override {
primitive_base<kv_cache>::save(ob);
ov::element::Type_t data_type = variable_info.data_type;
ob << variable_info.variable_id;
ob << variable_info.data_shape;
ob << make_data(&data_type, sizeof(ov::element::Type_t));
ob << concat_axis;
ob << gather_axis;
}
void load(BinaryInputBuffer& ib) override {
primitive_base<kv_cache>::load(ib);
ov::PartialShape data_shape;
ov::element::Type_t data_type;
std::string variable_id;
ib >> variable_id;
ib >> data_shape;
ib >> make_data(&data_type, sizeof(ov::element::Type_t));
variable_info = { data_shape, data_type, variable_id };
ib >> concat_axis;
ib >> gather_axis;
}
};
} // namespace cldnn
@@ -3,6 +3,8 @@
//
#include "prepare_buffer_fusing.h"
#include "pooling_inst.h"
#include "kv_cache_inst.h"
#include "gather_inst.h"
#include "primitive_inst.h"
#include "activation_inst.h"
#include "concatenation_inst.h"
@@ -447,7 +449,7 @@ void prepare_buffer_fusing::run(program& p) {
bool is_dynamic = node->is_dynamic();
bool is_planar = format::is_default_format(node->get_output_layout().format);
bool no_pad = !node->get_output_layout().data_padding && !node->get_input_layouts().empty() && !node->get_input_layout(0).data_padding;
if (node->is_type<read_value>())
if (node->is_type<read_value>() || node->is_type<kv_cache>())
return true;
if (node->is_type<reshape>() && is_dynamic && is_planar && no_pad && !node->is_output() && !node->has_fused_primitives()) {
@@ -620,6 +622,63 @@ void prepare_buffer_fusing::run(program& p) {
node.can_be_optimized(can_reshape_be_optimized(node));
});
program_helpers::do_for_types<kv_cache>(*node, [](kv_cache_node& node) {
auto kv_out_layout = node.get_output_layout();
program_node* rv_prim = nullptr;
program_node* gather_prim = nullptr;
if (node.get_dependency(0).is_type<read_value>()) {
rv_prim = &node.get_dependency(0);
} else {
if (node.get_dependency(0).is_type<gather>()) {
gather_prim = &node.get_dependency(0);
} else {
return;
}
if (gather_prim->get_dependency(0).is_type<read_value>()) {
rv_prim = &gather_prim->get_dependency(0);
}
}
if (!rv_prim)
return;
if (kv_out_layout.data_type != rv_prim->get_output_layout().data_type)
return;
auto concat_axis_legacy = node.get_primitive()->concat_axis;
if (concat_axis_legacy >= 2) {
auto spatial_axis = concat_axis_legacy - 2;
// Default and minimum number of dimensions is 4
auto spatial_size = std::max<size_t>(kv_out_layout.get_partial_shape().size(), 4) - 2;
concat_axis_legacy = spatial_size - spatial_axis - 1 + 2;
}
if (kv_out_layout.is_dynamic()) {
// set dynamic pad dims for shape agnostic kernel
auto info_dynamic_pad = tensor(0).sizes();
info_dynamic_pad[concat_axis_legacy] = 1;
auto dynamic_pad_mask = tensor(info_dynamic_pad);
kv_out_layout.data_padding.set_dynamic_pad(dynamic_pad_mask);
node.set_output_layout(kv_out_layout);
node.can_share_buffer(false);
auto update_dep = [&dynamic_pad_mask](program_node* dep) {
auto prev_layout = dep->get_output_layout();
prev_layout.data_padding.set_dynamic_pad(dynamic_pad_mask);
dep->set_output_layout(prev_layout);
dep->can_share_buffer(false);
};
if (rv_prim) {
update_dep(rv_prim);
}
if (gather_prim) {
update_dep(gather_prim);
}
}
});
program_helpers::do_for_types<read_value>(*node, [](read_value_node& node) {
// Current implementation allows to avoid copy on read_value primitive
// only in cases when it has single user
@@ -0,0 +1,141 @@
// Copyright (C) 2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#include "intel_gpu/runtime/debug_configuration.hpp"
#include "primitive_base.hpp"
#include "kv_cache_inst.h"
#include "concatenation/concatenation_kernel_selector.h"
#include "concatenation/concatenation_kernel_base.h"
namespace cldnn {
namespace ocl {
namespace {
kernel_selector::concat_axis convert_axis(int64_t axis, size_t rank) {
auto cldnn_axis = axis >= 0 ? axis : axis + static_cast<int64_t>(rank);
if (cldnn_axis >= static_cast<int64_t>(rank))
OPENVINO_THROW("kv_cache axis exceeds number of dimensions");
// Difference in dimension ordering between IE and GPU plugin,
// reverse spatial dimensions after batch and feature.
if (cldnn_axis >= 2) {
auto spatial_axis = cldnn_axis - 2;
// Default and minimum number of dimensions is 4
auto spatial_size = std::max<size_t>(rank, 4) - 2;
cldnn_axis = spatial_size - spatial_axis - 1 + 2;
}
switch (cldnn_axis) {
case 0: return kernel_selector::concat_axis::BATCH;
case 1: return kernel_selector::concat_axis::FEATURE;
case 2: return kernel_selector::concat_axis::X;
case 3: return kernel_selector::concat_axis::Y;
case 4: return kernel_selector::concat_axis::Z;
case 5: return kernel_selector::concat_axis::W;
default: OPENVINO_THROW("Unsupported kv_cache axis: ", axis);
}
return kernel_selector::concat_axis::FEATURE; // shouldn't get here
}
} // namespace
struct kv_cache_impl : typed_primitive_impl_ocl<kv_cache> {
using parent = typed_primitive_impl_ocl<kv_cache>;
using parent::parent;
using kernel_selector_t = kernel_selector::concatenation_kernel_selector;
using kernel_params_t = std::pair<kernel_selector::concatenation_params, kernel_selector::concatenation_optional_params>;
DECLARE_OBJECT_TYPE_SERIALIZATION(cldnn::ocl::kv_cache_impl)
std::unique_ptr<primitive_impl> clone() const override {
return make_unique<kv_cache_impl>(*this);
}
void load(BinaryInputBuffer& ib) override {
parent::load(ib);
if (is_dynamic()) {
auto& kernel_selector = kernel_selector_t::Instance();
auto kernel_impl = kernel_selector.GetImplementation(_kernel_data.kernelName);
kernel_impl->GetUpdateDispatchDataFunc(_kernel_data);
}
}
kernel_arguments_data get_arguments(const kv_cache_inst& instance) const override {
kernel_arguments_data args = parent::get_arguments(instance);
args.inputs = { instance.input_memory_ptr(0), instance.input_memory_ptr(1) };
return args;
}
event::ptr execute_impl(const std::vector<event::ptr>& events, kv_cache_inst& instance) override {
const bool can_be_optimized = instance.get_impl_params()->_can_be_optimized;
const auto& desc = instance.get_typed_desc<kv_cache>();
auto& variable = instance.get_network().get_variable(desc->variable_info.variable_id);
auto res_event = parent::execute_impl(events, instance);
variable.set();
if (can_be_optimized) {
// When primitive is optimized, concat kernel writes directly to variable memory
return res_event;
} else {
// Othwerise, we need to copy result from out buffer to state memory
auto& stream = instance.get_network().get_stream();
stream.enqueue_barrier();
return variable.get_memory()->copy_from(stream, instance.output_memory(0), false);
}
}
static kernel_params_t get_kernel_params(const kernel_impl_params& impl_param, bool is_shape_agnostic = false) {
const auto& primitive = impl_param.typed_desc<kv_cache>();
auto params = get_default_params<kernel_selector::concatenation_params>(impl_param, is_shape_agnostic);
auto optional_params = get_default_optional_params<kernel_selector::concatenation_optional_params>(impl_param.get_program());
auto axis = primitive->concat_axis;
const auto inputs_count = primitive->input_size();
params.inputs.resize(inputs_count);
for (size_t i = 0; i < inputs_count; ++i) {
params.inputs[i] = convert_data_tensor(impl_param.input_layouts[i]);
}
params.axis = convert_axis(axis, impl_param.get_output_layout().get_rank());
optional_params.kernelPerInput = true;
return {params, optional_params};
}
void update_dispatch_data(const kernel_impl_params& impl_param) override {
auto kernel_params = get_kernel_params(impl_param, true);
(_kernel_data.update_dispatch_data_func)(kernel_params.first, _kernel_data);
_kernel_data.kernels[0].skip_execution = impl_param._can_be_optimized || impl_param.get_input_layout(0).count() == 0;
}
};
namespace detail {
attach_kv_cache_impl::attach_kv_cache_impl() {
auto types = { data_types::f16, data_types::f32 };
auto formats = { format::bfyx };
implementation_map<kv_cache>::add(impl_types::ocl,
shape_types::dynamic_shape,
typed_primitive_impl_ocl<kv_cache>::create<kv_cache_impl>,
types,
formats);
implementation_map<kv_cache>::add(impl_types::ocl,
shape_types::static_shape,
typed_primitive_impl_ocl<kv_cache>::create<kv_cache_impl>,
types,
formats);
}
} // namespace detail
} // namespace ocl
} // namespace cldnn
BIND_BINARY_BUFFER_WITH_TYPE(cldnn::ocl::kv_cache_impl)
BIND_BINARY_BUFFER_WITH_TYPE(cldnn::kv_cache)
@@ -40,6 +40,7 @@ void register_implementations() {
REGISTER_OCL(generate_proposals);
REGISTER_OCL(grid_sample);
REGISTER_OCL(group_normalization);
REGISTER_OCL(kv_cache);
REGISTER_OCL(lrn);
REGISTER_OCL(lstm_gemm);
REGISTER_OCL(lstm_elt);
@@ -77,6 +77,7 @@
#include "intel_gpu/primitives/non_zero.hpp"
#include "intel_gpu/primitives/eye.hpp"
#include "intel_gpu/primitives/unique.hpp"
#include "intel_gpu/primitives/kv_cache.hpp"
namespace cldnn {
namespace ocl {
@@ -122,6 +123,7 @@ REGISTER_OCL(gemm);
REGISTER_OCL(generate_proposals);
REGISTER_OCL(grid_sample);
REGISTER_OCL(group_normalization);
REGISTER_OCL(kv_cache);
REGISTER_OCL(lrn);
REGISTER_OCL(lstm_gemm);
REGISTER_OCL(lstm_elt);
@@ -0,0 +1,45 @@
// Copyright (C) 2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#pragma once
#include "intel_gpu/primitives/kv_cache.hpp"
#include "primitive_inst.h"
#include "variable.hpp"
namespace cldnn {
template <>
struct typed_program_node<kv_cache> : public typed_program_node_base<kv_cache> {
private:
using parent = typed_program_node_base<kv_cache>;
public:
using parent::parent;
program_node& input() const { return get_dependency(0); }
std::vector<size_t> get_shape_infer_dependencies() const override { return {}; }
};
using kv_cache_node = typed_program_node<kv_cache>;
template<>
class typed_primitive_inst<kv_cache> : public typed_primitive_inst_base<kv_cache>, public memory_state::variable {
using parent = typed_primitive_inst_base<kv_cache>;
public:
template<typename ShapeType>
static std::vector<layout> calc_output_layouts(kv_cache_node const& /*node*/, const kernel_impl_params& impl_param);
static layout calc_output_layout(const kv_cache_node& node, kernel_impl_params const& impl_param);
static std::string to_string(const kv_cache_node& node);
typed_primitive_inst(network& network, const kv_cache_node& desc);
typed_primitive_inst(network& network) : parent(network), memory_state::variable("") {}
};
using kv_cache_inst = typed_primitive_inst<kv_cache>;
} // namespace cldnn
@@ -232,6 +232,7 @@ public:
void do_runtime_skip_reorder();
void do_runtime_skip_gather();
void do_runtime_in_place_concat();
void do_runtime_in_place_kv_cache();
void configure_shape_of_dependencies();
memory::ptr fused_memory(size_t dep_id) const {
@@ -0,0 +1,58 @@
// Copyright (C) 2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#include "intel_gpu/op/kv_cache.hpp"
#include "intel_gpu/plugin/common_utils.hpp"
#include "intel_gpu/runtime/optionals.hpp"
#include "kv_cache_inst.h"
#include "primitive_type_base.h"
#include <sstream>
#include <json_object.h>
namespace cldnn {
GPU_DEFINE_PRIMITIVE_TYPE_ID(kv_cache)
kv_cache_inst::typed_primitive_inst(network& network, const kv_cache_node& node) :
parent{network, node, false},
memory_state::variable{node.get_primitive()->variable_info.variable_id} {
}
layout kv_cache_inst::calc_output_layout(const kv_cache_node& node, kernel_impl_params const& impl_param) {
return impl_param.input_layouts[0];
}
template<typename ShapeType>
std::vector<layout> kv_cache_inst::calc_output_layouts(kv_cache_node const& /*node*/, kernel_impl_params const& impl_param) {
auto desc = impl_param.typed_desc<kv_cache>();
auto output_data_type = desc->output_data_types[0].value_or(impl_param.get_input_layout().data_type);
ov::intel_gpu::op::KVCache op;
op.set_concat_axis(desc->concat_axis);
op.set_gather_axis(desc->gather_axis);
std::vector<ShapeType> input_shapes = {impl_param.get_input_layout(0).get<ShapeType>(), impl_param.get_input_layout(1).get<ShapeType>()};
std::vector<ShapeType> output_shapes = shape_infer(&op, input_shapes);
return {layout({output_shapes[0], output_data_type, impl_param.get_output_layout().format})};
}
template std::vector<layout> kv_cache_inst::calc_output_layouts<ov::PartialShape>(kv_cache_node const& node, const kernel_impl_params& impl_param);
std::string kv_cache_inst::to_string(const kv_cache_node& node) {
auto node_info = node.desc_to_json();
json_composite kv_cache_info;
kv_cache_info.add("input id", node.input().id());
kv_cache_info.add("variable id", node.get_primitive()->variable_info.variable_id);
kv_cache_info.add("variable shape", node.get_primitive()->variable_info.data_shape);
kv_cache_info.add("variable type", node.get_primitive()->variable_info.data_type);
kv_cache_info.add("concat axis", node.get_primitive()->concat_axis);
kv_cache_info.add("gather axis", node.get_primitive()->gather_axis);
node_info->add("kv_cache info", kv_cache_info);
std::stringstream primitive_description;
node_info->dump(primitive_description);
return primitive_description.str();
}
} // namespace cldnn
@@ -25,6 +25,7 @@
#include "gemm_inst.h"
#include "assign_inst.h"
#include "read_value_inst.h"
#include "kv_cache_inst.h"
#include "condition_inst.h"
#include "gather_inst.h"
#include "experimental_detectron_roi_feature_extractor_inst.hpp"
@@ -425,7 +426,14 @@ void primitive_inst::update_shape() {
if (get_node().is_type<assign>()) {
auto desc = get_node().as<assign>().get_primitive();
get_network().get_variable(desc->variable_id).set_layout(_impl_params->get_output_layout());
_impl_params->state_layout = _impl_params->get_output_layout();
}
if (get_node().is_type<read_value>()) {
auto desc = get_node().as<read_value>().get_primitive();
auto& variable = get_network().get_variable(desc->variable_id);
auto variable_layout = variable.get_layout();
// Custom output layout update as update_output_layout handles paddings incorrectly for optimized out read_value + kv_cache pattern
_impl_params->output_layouts[0] = variable_layout;
}
}
@@ -460,8 +468,20 @@ event::ptr primitive_inst::realloc_if_needed() {
if (auto stateful_prim = dynamic_cast<memory_state::variable*>(this)) {
std::string variable_id = stateful_prim->variable_id();
auto& variable = get_network().get_variable(variable_id);
GPU_DEBUG_TRACE_DETAIL << "realloc_if_needed: variable " << id() << " set layout" << _impl_params->get_output_layout().to_string() << std::endl;
if (_node->is_type<kv_cache>()) {
// Reuse state memory as output for kv cache if possible
// otherwise clear _outputs for the cases when mem was reused previously
if (_impl_params->can_be_optimized()) {
_outputs[0] = variable.get_memory();
return ev;
} else if (_outputs[0] && variable.get_memory() && get_network().get_engine().is_the_same_buffer(*_outputs[0], *variable.get_memory())) {
_outputs[0] = nullptr;
max_output_layout_size = 0;
}
}
variable.set_layout(actual_layout);
GPU_DEBUG_TRACE_DETAIL << id() << ": use variable memory " << variable.get_memory()
GPU_DEBUG_TRACE_DETAIL << id() << ": use variable memory " << variable.get_memory()->buffer_ptr()
<< " (size=" << variable.get_memory()->size() << ")" << std::endl;
// For nodes that can be optimized, variable memory is used as output memory
// so there is no need for output memory reallocation
@@ -488,6 +508,19 @@ event::ptr primitive_inst::realloc_if_needed() {
}
}
// Clear out memory if if was previously reused, but now primitive can't be optimized
if (_node->is_type<gather>() && !can_be_optimized() && _outputs[0] && _network.get_engine().is_the_same_buffer(dep_memory(0), output_memory(0))) {
_outputs[0] = nullptr;
max_output_layout_size = 0;
}
// update layout to ensure that it repsects paddings for correct allocation size
if (_node->is_type<kv_cache>() && !_impl_params->can_be_optimized()) {
const auto current_buf_size = updated_layout.get_buffer_size().sizes();
ov::Shape current_shape(current_buf_size.begin(), current_buf_size.end());
updated_layout.set_partial_shape(current_shape);
}
bool can_reuse_buffer = _outputs[0] && updated_layout.count() <= max_output_layout_size;
// Handle runtime dynamic concat optimization
@@ -790,6 +823,95 @@ void primitive_inst::do_runtime_skip_reorder() {
}
}
void primitive_inst::do_runtime_in_place_kv_cache() {
OV_ITT_SCOPED_TASK(ov::intel_gpu::itt::domains::intel_gpu_plugin, openvino::itt::handle("do_runtime_in_place_kv_cache: " + id()));
auto reset_pad = [](kernel_impl_params& params, const program_node* node) {
params.output_layouts[0].data_padding = node->get_output_layout(0).data_padding;
};
if (_node->is_type<read_value>()) {
auto& variable = get_network().get_variable(_node->as<read_value>().get_primitive()->variable_id);
// Reset paddings for read_value and users with dynamic pad when variable is reset
// to avoid wrong pad used for some nodes due to pad propagation logic (which uses previous iter pad values)
if (!variable.is_set()) {
primitive_inst* inst = this;
while (inst) {
reset_pad(*inst->_impl_params, inst->_node);
auto& users = inst->_node->get_users();
if (users.size() == 1 && users.front()->get_output_layout(0).data_padding.get_dynamic_pad_dims() != tensor(0)) {
inst = inst->get_user_insts().front().get();
} else {
inst = nullptr;
}
}
}
return;
}
if (_node->is_type<gather>() && _impl_params->output_layouts[0].data_padding.get_dynamic_pad_dims() != tensor(0)) {
if (can_be_optimized())
_impl_params->output_layouts[0] = _impl_params->input_layouts[0];
else
reset_pad(*_impl_params, _node);
return;
}
if (!_node->is_type<kv_cache>())
return;
_impl_params->_can_be_optimized = false;
if (_impl_params->get_input_layout(0).count() == 0) {
return;
}
auto desc = _node->as<kv_cache>().get_primitive();
auto& past_layout = _impl_params->input_layouts[0];
auto& present_layout = _impl_params->output_layouts[0];
const auto& sequence_axis = desc->concat_axis;
auto sequence_axis_legacy = sequence_axis;
if (sequence_axis_legacy >= 2) {
auto spatial_axis = sequence_axis_legacy - 2;
// Default and minimum number of dimensions is 4
auto spatial_size = std::max<size_t>(past_layout.get_partial_shape().size(), 4) - 2;
sequence_axis_legacy = spatial_size - spatial_axis - 1 + 2;
}
if (present_layout.data_padding.get_dynamic_pad_dims().sizes()[sequence_axis_legacy] != 1)
return;
const size_t total_elements = past_layout.count();
const int64_t concat_axis_size = past_layout.get_partial_shape()[sequence_axis].get_length();
const int64_t sequence_element_size = total_elements / concat_axis_size;
const int64_t max_sequence_elements = _deps[0].first->max_output_layout_size / sequence_element_size;
const int64_t max_pad = std::max<int64_t>(max_sequence_elements - concat_axis_size, 0);
if (max_pad > 0) {
auto update_pad = [&](layout& l, int64_t pad) {
const auto& dyn_pad_dims = l.data_padding.get_dynamic_pad_dims();
const auto& lower_padd = l.data_padding.lower_size().sizes();
auto upper_padd = l.data_padding.upper_size().sizes();
upper_padd[sequence_axis_legacy] = pad;
l.data_padding = padding(lower_padd, upper_padd, 0.f, dyn_pad_dims);
};
update_pad(present_layout, max_pad - 1);
auto& variable = get_network().get_variable(desc->variable_info.variable_id);
GPU_DEBUG_TRACE_DETAIL << "do_runtime_in_place_kv_cache set_layout: " << present_layout.to_string() << " is_set = " << variable.is_set() << std::endl;
variable.set_layout(present_layout);
if (past_layout.data_padding.upper_size().sizes()[sequence_axis_legacy] > 0 && variable.is_set()) {
update_pad(past_layout, max_pad);
_impl_params->_can_be_optimized = true;
}
GPU_DEBUG_TRACE_DETAIL << "[do runtime kv_cache opt] concat_axis_size = " << concat_axis_size << std::endl;
GPU_DEBUG_TRACE_DETAIL << "[do runtime kv_cache opt] sequence_element_size = " << sequence_element_size << std::endl;
GPU_DEBUG_TRACE_DETAIL << "[do runtime kv_cache opt] max_sequence_elements = " << max_sequence_elements << std::endl;
GPU_DEBUG_TRACE_DETAIL << "[do runtime kv_cache opt] max_pad = " << max_pad << std::endl;
GPU_DEBUG_TRACE_DETAIL << "[do runtime kv_cache opt] can be optimized: " << _impl_params->_can_be_optimized << std::endl;
GPU_DEBUG_TRACE_DETAIL << "[do runtime kv_cache opt] " << present_layout.to_string() << std::endl;
}
}
void primitive_inst::do_runtime_skip_gather() {
// Check pattern
if (!get_node().is_type<gather>()
@@ -932,6 +1054,7 @@ event::ptr primitive_inst::execute(const std::vector<event::ptr>& events) {
// if the user is can_be_optimized and output node then current nodes' output should be allocated to host.
do_runtime_skip_reorder();
do_runtime_skip_gather();
do_runtime_in_place_kv_cache();
if (_impl_params->output_layouts[0].count() == 0) {
GPU_DEBUG_TRACE_DETAIL << id() << " : Skipping because output data is empty " << std::endl;
auto ev = get_network().get_stream().create_user_event(true);
@@ -0,0 +1,40 @@
// Copyright (C) 2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#include "intel_gpu/op/kv_cache.hpp"
#include "intel_gpu/plugin/program_builder.hpp"
#include "intel_gpu/plugin/common_utils.hpp"
#include "intel_gpu/primitives/kv_cache.hpp"
namespace ov {
namespace op {
namespace internal {
using KVCache = ov::intel_gpu::op::KVCache;
} // namespace internal
} // namespace op
} // namespace ov
namespace ov {
namespace intel_gpu {
namespace {
void CreateKVCacheOp(ProgramBuilder& p, const std::shared_ptr<ov::op::internal::KVCache>& op) {
validate_inputs_count(op, {2, 3});
auto inputs = p.GetInputInfo(op);
const auto prim = cldnn::kv_cache(layer_type_name_ID(op),
inputs,
op->get_variable()->get_info(),
op->get_concat_axis(),
op->get_gather_axis());
p.add_primitive(*op, prim);
}
} // namespace
REGISTER_FACTORY_IMPL(internal, KVCache);
} // namespace intel_gpu
} // namespace ov
@@ -8,9 +8,18 @@
#include "openvino/op/assign.hpp"
#include "openvino/op/read_value.hpp"
#include "transformations/rt_info/original_precision_attribute.hpp"
#include "intel_gpu/op/read_value.hpp"
#include "intel_gpu/primitives/assign.hpp"
#include "intel_gpu/primitives/read_value.hpp"
namespace ov {
namespace op {
namespace internal {
using ReadValue = ov::intel_gpu::op::ReadValue;
} // namespace internal
} // namespace op
} // namespace ov
namespace ov {
namespace intel_gpu {
@@ -41,6 +50,11 @@ void CreateReadValueOp(ProgramBuilder& p, const std::shared_ptr<ov::op::v3::Read
CreateVariableAccessPrimitive<cldnn::read_value>(p, op, op->get_variable_id());
}
void CreateReadValueOp(ProgramBuilder& p, const std::shared_ptr<ov::intel_gpu::op::ReadValue>& op) {
validate_inputs_count(op, {0, 1});
CreateVariableAccessPrimitive<cldnn::read_value>(p, op, op->get_variable_id());
}
void CreateAssignOp(ProgramBuilder& p, const std::shared_ptr<ov::op::v3::Assign>& op) {
validate_inputs_count(op, {1});
CreateVariableAccessPrimitive<cldnn::assign>(p, op, op->get_variable_id());
@@ -62,6 +76,7 @@ REGISTER_FACTORY_IMPL(v3, Assign);
REGISTER_FACTORY_IMPL(v6, Assign);
REGISTER_FACTORY_IMPL(v3, ReadValue);
REGISTER_FACTORY_IMPL(v6, ReadValue);
REGISTER_FACTORY_IMPL(internal, ReadValue);
} // namespace intel_gpu
} // namespace ov
@@ -0,0 +1,144 @@
// Copyright (C) 2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#include "kv_cache_fusion.hpp"
#include <memory>
#include "intel_gpu/op/kv_cache.hpp"
#include "intel_gpu/op/read_value.hpp"
#include "intel_gpu/plugin/common_utils.hpp"
#include "openvino/core/node_vector.hpp"
#include "openvino/core/rt_info.hpp"
#include "openvino/op/constant.hpp"
#include "openvino/op/convert.hpp"
#include "openvino/op/parameter.hpp"
#include "openvino/op/read_value.hpp"
#include "openvino/op/sink.hpp"
#include "openvino/pass/graph_rewrite.hpp"
#include "openvino/pass/pattern/op/label.hpp"
#include "openvino/pass/pattern/op/wrap_type.hpp"
#include "openvino/pass/pattern/op/or.hpp"
#include "openvino/pass/visualize_tree.hpp"
#include "transformations/utils/utils.hpp"
namespace ov {
namespace intel_gpu {
class KVCacheFusionMatcher : public ov::pass::MatcherPass {
public:
OPENVINO_RTTI("KVCacheFusionMatcher", "0");
KVCacheFusionMatcher();
};
KVCacheFusionMatcher::KVCacheFusionMatcher() {
using namespace ov::pass::pattern;
auto past = wrap_type<ov::op::v6::ReadValue>();
auto convert_past = wrap_type<ov::op::v0::Convert>({past});
auto gather_input = std::make_shared<ov::pass::pattern::op::Or>(OutputVector{past, convert_past});
auto beam_idx = wrap_type<ov::op::v0::Parameter>();
auto gather_past = wrap_type<ov::op::v8::Gather>({gather_input, beam_idx, wrap_type<ov::op::v0::Constant>()});
auto concat_past_input = std::make_shared<ov::pass::pattern::op::Or>(OutputVector{past, convert_past, gather_past});
auto concat = wrap_type<ov::op::v0::Concat>({concat_past_input, any_input()});
auto convert_present = wrap_type<ov::op::v0::Convert>({concat});
auto present_input = std::make_shared<ov::pass::pattern::op::Or>(OutputVector{concat, convert_present});
auto present = wrap_type<ov::op::v6::Assign>({present_input});
ov::matcher_pass_callback callback = [=](ov::pass::pattern::Matcher& m) {
if (transformation_callback(m.get_match_root())) {
return false;
}
const auto& pattern_map = m.get_pattern_value_map();
auto concat_node = std::dynamic_pointer_cast<ov::op::v0::Concat>(pattern_map.at(concat).get_node_shared_ptr());
auto past_node = std::dynamic_pointer_cast<ov::op::v6::ReadValue>(pattern_map.at(past).get_node_shared_ptr());
auto present_node = std::dynamic_pointer_cast<ov::op::v6::Assign>(pattern_map.at(present).get_node_shared_ptr());
if (past_node->get_variable_id() != present_node->get_variable_id())
return false;
// TODO: Support conversion internally
if (concat_node->get_output_element_type(0) != past_node->get_output_element_type(0))
return false;
auto variable = past_node->get_variable();
auto concat_axis = concat_node->get_axis();
std::shared_ptr<ov::Node> variable_initializer = nullptr;
std::shared_ptr<ov::Node> kv_cache_node = nullptr;
if (past_node->get_input_size() == 1) {
variable_initializer = past_node->get_input_node_shared_ptr(0);
}
// Replace common ReadValue op with a custom one as common one expects paired Assign operation which is removed by this transform
auto new_read_value_node = variable_initializer ? std::make_shared<ov::intel_gpu::op::ReadValue>(variable_initializer, variable)
: std::make_shared<ov::intel_gpu::op::ReadValue>(variable);
new_read_value_node->set_friendly_name(past_node->get_friendly_name());
ov::copy_runtime_info(past_node, new_read_value_node);
ov::replace_node(past_node, new_read_value_node);
if (pattern_map.count(gather_past) > 0) {
// TODO: Enable code below once KVCache custom op supports rearrange internally
// For now Gather is kept as standalone op
#if 0
auto gather_node = pattern_map.at(gather_past).get_node_shared_ptr();
auto gather_axis = std::dynamic_pointer_cast<ov::op::v0::Constant>(gather_node->get_input_node_shared_ptr(2))->cast_vector<int64_t>()[0];
kv_cache_node = std::make_shared<op::KVCache>(new_read_value_node,
concat_node->get_input_node_shared_ptr(1),
gather_node->get_input_node_shared_ptr(1),
variable,
concat_axis,
gather_axis,
concat_node->get_output_element_type(0));
#else
kv_cache_node = std::make_shared<op::KVCache>(pattern_map.at(gather_past).get_node_shared_ptr(),
concat_node->get_input_node_shared_ptr(1),
variable,
concat_axis,
new_read_value_node->get_output_element_type(0));
#endif
} else {
kv_cache_node = std::make_shared<op::KVCache>(new_read_value_node,
concat_node->get_input_node_shared_ptr(1),
variable,
concat_axis,
new_read_value_node->get_output_element_type(0));
}
kv_cache_node->set_friendly_name(concat_node->get_friendly_name());
ov::copy_runtime_info(m.get_matched_nodes(), kv_cache_node);
ov::replace_node(concat_node, kv_cache_node);
if (pattern_map.count(convert_present) > 0) {
present_node->set_argument(0, kv_cache_node->output(0));
}
return true;
};
auto m = std::make_shared<ov::pass::pattern::Matcher>(present, "KVCacheFusionMatcher");
this->register_matcher(m, callback);
}
bool KVCacheFusion::run_on_model(const std::shared_ptr<ov::Model>& m) {
bool res = pass::GraphRewrite::run_on_model(m);
if (res) {
ov::SinkVector sinks = m->get_sinks();
for (auto& sink : sinks) {
if (sink && sink->get_input_node_ptr(0)->get_type_info() == op::KVCache::get_type_info_static()) {
m->remove_sink(sink);
}
}
}
return res;
}
KVCacheFusion::KVCacheFusion() {
add_matcher<ov::intel_gpu::KVCacheFusionMatcher>();
}
} // namespace intel_gpu
} // namespace ov
@@ -0,0 +1,124 @@
// Copyright (C) 2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#pragma once
#include "openvino/pass/graph_rewrite.hpp"
namespace ov {
namespace intel_gpu {
/// 1. Trivial case (greedy search, no state initializer)
/// ┌───────────┐ ┌───────────┐ ┌───────────┐
/// │ ReadValue │ │ SomeOp │ │ SomeOp │
/// | (past_kv) | | new_token | | new_token |
/// └─────┬─────┘ └─────┬─────┘ └─────┬─────┘
/// │ │ |
/// │ | |
/// │ ┌────────┐ │ ┌────┴────┐ ┌──────────┐
/// └───┤ Concat ├─────┘ => | KVCache |.......| Variable |
/// └───┬────┘ └────┬────┘ └──────────┘
/// │ |
/// ┌──────────┴────────────┐ |
/// ┌────┴────────┐ ┌────┴──────┐ ┌────┴────┐
/// │ Assign │ │ SomeOp │ | SomeOp |
/// | (present_kv | | (SDPA) | | (SDPA) |
/// └─────────────┘ └───────────┘ └─────────┘
/// 2. With gather for beam search (or model which supports both greedy and beam search)
/// ┌───────────┐ ┌────────────┐ ┌───────────┐ ┌───────────┐ ┌────────────┐
/// │ ReadValue │ │ Parameter │ │ SomeOp │ │ SomeOp │ │ Parameter │
/// | (past_kv) | | beam_idx | | new_token | | new_token | | beam_idx |
/// └─────┬─────┘ └─────┬──────┘ └─────┬─────┘ └─────┬─────┘ └─────┬──────┘
/// │ │ │ | |
/// ┌─────┴──────┐ | │ | |
/// | Gather |───────────┘ │ | |
/// └─────┬──────┘ │ | |
/// | │ | |
/// | │ | |
/// │ │ | |
/// │ ┌────────┐ │ | ┌────┴────┐ ┌──────────┐
/// └───┤ Concat ├───────────────────────┘ => └────────────┤ KVCache |......| Variable |
/// └───┬────┘ └────┬────┘ └──────────┘
/// │ |
/// ┌──────────┴────────────┐ |
/// ┌────┴────────┐ ┌────┴──────┐ ┌────┴────┐
/// │ Assign │ │ SomeOp │ | SomeOp |
/// | (present_kv | | (SDPA) | | (SDPA) |
/// └─────────────┘ └───────────┘ └─────────┘
/// 3. Similar to case 2, but with variable initializer
/// ┌─────────────────────┐ ┌─────────────────────┐
/// │ SomeOp │ │ SomeOp │
/// | (state initializer) | | (state initializer) |
/// └─────┬───────────────┘ └─────┬───────────────┘
/// | |
/// ┌─────┴─────┐ ┌────────────┐ ┌───────────┐ ┌───────────┐ ┌────────────┐ |
/// │ ReadValue │ │ Parameter │ │ SomeOp │ │ SomeOp │ │ Parameter │ |
/// | (past_kv) | | beam_idx | | new_token | | new_token | | beam_idx | |
/// └─────┬─────┘ └─────┬──────┘ └─────┬─────┘ └─────┬─────┘ └─────┬──────┘ |
/// │ │ │ | | ┌─────────┘
/// ┌─────┴──────┐ | │ | | |
/// | Gather |───────────┘ │ | | |
/// └─────┬──────┘ │ | | |
/// | │ | | |
/// | │ | | |
/// │ │ | | |
/// │ ┌────────┐ │ | ┌────┴──────┴───────┐ ┌──────────┐
/// └───┤ Concat ├───────────────────────┘ => └────────────┤ KVCache |.......................| Variable |
/// └───┬────┘ └────┬──────────────┘ └──────────┘
/// │ |
/// ┌──────────┴────────────┐ |
/// ┌────┴────────┐ ┌────┴──────┐ ┌────┴────┐
/// │ Assign │ │ SomeOp │ | SomeOp |
/// | (present_kv | | (SDPA) | | (SDPA) |
/// └─────────────┘ └───────────┘ └─────────┘
/// 4. Pattern may also include Convert for state variable
/// ┌─────────────────────┐ ┌─────────────────────┐
/// │ SomeOp │ │ SomeOp │
/// | (state initializer) | | (state initializer) |
/// └─────┬───────────────┘ └─────┬───────────────┘
/// | |
/// ┌─────┴─────┐ ┌────────────┐ ┌───────────┐ ┌───────────┐ ┌────────────┐ |
/// │ ReadValue │ │ Parameter │ │ SomeOp │ │ SomeOp │ │ Parameter │ |
/// | (past_kv) | | beam_idx | | new_token | | new_token | | beam_idx | |
/// └─────┬─────┘ └─────┬──────┘ └─────┬─────┘ └─────┬─────┘ └─────┬──────┘ |
/// │ │ │ | | ┌─────────┘
/// ┌─────┴────┐ │ │ | | |
/// │ Convert │ │ │ | | |
/// └─────┬────┘ │ │ | | |
/// │ │ │ | | |
/// │ │ │ | | |
/// ┌─────┴──────┐ | │ | | |
/// | Gather |───────────┘ │ | | |
/// └─────┬──────┘ │ | | |
/// | │ | | |
/// | │ | | |
/// │ │ | | |
/// │ ┌────────┐ │ | ┌────┴──────┴───────┐ ┌──────────┐
/// └───┤ Concat ├───────────────────────┘ => └────────────┤ KVCache |.......................| Variable |
/// └───┬────┘ └────┬──────────────┘ └──────────┘
/// │ |
/// ┌──────────┴────────────┐ |
/// | | |
/// ┌─────┴────┐ | |
/// │ Convert │ | |
/// └─────┬────┘ | |
/// | | |
/// ┌────┴────────┐ ┌────┴──────┐ ┌────┴────┐
/// │ Assign │ │ SomeOp │ | SomeOp |
/// | (present_kv | | (SDPA) | | (SDPA) |
/// └─────────────┘ └───────────┘ └─────────┘
class KVCacheFusion : public ov::pass::GraphRewrite {
public:
OPENVINO_RTTI("KVCacheFusion", "0");
KVCacheFusion();
bool run_on_model(const std::shared_ptr<ov::Model>& m) override;
};
} // namespace intel_gpu
} // namespace ov
@@ -0,0 +1,78 @@
// Copyright (C) 2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#include "intel_gpu/op/kv_cache.hpp"
#include "concat_shape_inference.hpp"
#include "openvino/core/partial_shape.hpp"
#include "openvino/op/concat.hpp"
namespace ov {
namespace intel_gpu {
namespace op {
KVCache::KVCache(const Output<Node>& past,
const Output<Node>& new_token_data,
const Output<Node>& beam_idx,
const std::shared_ptr<ov::op::util::Variable>& past_variable,
int64_t concat_axis,
int64_t gather_axis,
const ov::element::Type output_type)
: Op({past, new_token_data, beam_idx}), m_concat_axis(concat_axis), m_gather_axis(gather_axis), m_output_type(output_type) {
m_variable = past_variable;
validate_and_infer_types();
}
KVCache::KVCache(const Output<Node>& past,
const Output<Node>& new_token_data,
const std::shared_ptr<ov::op::util::Variable>& past_variable,
int64_t concat_axis,
const ov::element::Type output_type)
: Op({past, new_token_data}), m_concat_axis(concat_axis), m_gather_axis(0), m_output_type(output_type) {
m_variable = past_variable;
validate_and_infer_types();
}
bool KVCache::visit_attributes(ov::AttributeVisitor& visitor) {
visitor.on_attribute("concat_axis", m_concat_axis);
visitor.on_attribute("gather_axis", m_gather_axis);
visitor.on_attribute("output_type", m_output_type);
return true;
}
void KVCache::validate_and_infer_types() {
auto output_type = m_output_type == ov::element::undefined ? get_input_element_type(0) : m_output_type;
const std::vector<ov::PartialShape> input_shapes = {get_input_partial_shape(0), get_input_partial_shape(1)};
set_output_type(0, output_type, shape_infer(this, input_shapes)[0]);
}
std::shared_ptr<Node> KVCache::clone_with_new_inputs(const ov::OutputVector& new_args) const {
check_new_args_count(this, new_args);
if (new_args.size() == 2) {
return std::make_shared<KVCache>(new_args.at(0),
new_args.at(1),
m_variable,
m_concat_axis,
m_output_type);
} else {
return std::make_shared<KVCache>(new_args.at(0),
new_args.at(1),
new_args.at(2),
m_variable,
m_concat_axis,
m_gather_axis,
m_output_type);
}
}
std::vector<ov::PartialShape> shape_infer(const KVCache* op, std::vector<ov::PartialShape> input_shapes) {
ov::op::v0::Concat concat;
concat.set_axis(op->get_concat_axis());
return ov::op::v0::shape_infer(&concat, input_shapes);
}
} // namespace op
} // namespace intel_gpu
} // namespace ov
@@ -0,0 +1,94 @@
// Copyright (C) 2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#include "intel_gpu/op/read_value.hpp"
#include "openvino/core/partial_shape.hpp"
namespace ov {
namespace intel_gpu {
namespace op {
ReadValue::ReadValue(const std::shared_ptr<ov::op::util::Variable>& past_values) : Op() {
m_variable = past_values;
validate_and_infer_types();
}
ReadValue::ReadValue(const Output<Node>& variable_initializer, const std::shared_ptr<ov::op::util::Variable>& past_values) : Op({variable_initializer}) {
m_variable = past_values;
validate_and_infer_types();
}
bool ReadValue::visit_attributes(ov::AttributeVisitor& visitor) {
visitor.on_attribute("variable_id", m_variable);
auto variable_info = m_variable->get_info();
visitor.on_attribute("variable_type", variable_info.data_type);
visitor.on_attribute("variable_shape", variable_info.data_shape);
m_variable->update(variable_info);
return true;
}
void ReadValue::validate_and_infer_types() {
OPENVINO_ASSERT(m_variable, "Variable is not initialized.");
const auto& variable_info = m_variable->get_info();
const auto& variable_type = variable_info.data_type;
const auto& variable_shape = variable_info.data_shape;
// If no inputs provided, it means this ReadValue doesn't have initial subgraph. This is valid.
if (get_input_size() > 0) {
const auto& initial_type = get_input_element_type(0);
const auto& initial_shape = get_input_partial_shape(0);
// Variable's shape/type determine a permissible range of values for shape/type inferred from initial_subgraph.
// If initial_subgraph is set, then we need to check that shape/type inferred from initial_subgraph
// is within the permissible range.
const auto compatible_type = variable_type.is_dynamic() || initial_type == variable_type;
const auto compatible_shape = variable_shape.relaxes(initial_shape);
OPENVINO_ASSERT(compatible_shape,
"The shape specified in the Variable has to extend (relax) the shape "
"inferred from the initializing subgraph.",
" Variable shape: ",
variable_shape,
" Initialization shape: ",
initial_shape);
OPENVINO_ASSERT(compatible_type,
"The type specified in the Variable has to extend (relax) the type "
"inferred from the initializing subgraph.",
" Variable type: ",
variable_type,
" Initialization type: ",
initial_type);
// workaround:
// dynamic rank/type can be derived from the IRs generated via the prev versions of OV,
// but dynamic rank/type are not supported in plugins,
// so we are trying to fix them here using the rank/type of ReadValue 1st input, if it exists
if (get_input_size() > 0 && variable_info.data_shape.rank().is_dynamic() &&
variable_info.data_type.is_dynamic()) {
set_output_type(0, initial_type, initial_shape);
return;
}
}
set_output_type(0, variable_type, variable_shape);
}
std::shared_ptr<Node> ReadValue::clone_with_new_inputs(const ov::OutputVector& new_args) const {
check_new_args_count(this, new_args);
switch (new_args.size()) {
case 0:
return std::make_shared<ReadValue>(m_variable);
case 1:
return std::make_shared<ReadValue>(new_args[0], m_variable);
default:
OPENVINO_THROW("Unable to clone ReadValue ",
this->get_friendly_name(),
" Incorrect number of inputs. Expected: 0 or 1. Actual: ",
new_args.size());
}
}
} // namespace op
} // namespace intel_gpu
} // namespace ov
@@ -121,6 +121,7 @@
#include "plugin/transformations/rms_fusion.hpp"
#include "plugin/transformations/binary_conv_to_conv.hpp"
#include "plugin/transformations/move_convert_after_gather.hpp"
#include "plugin/transformations/kv_cache_fusion.hpp"
#include "transformations/low_precision/mark_dequantization_subgraph.hpp"
#include "low_precision/pull_reshape_through_dequantization.hpp"
@@ -703,6 +704,7 @@ void TransformationsPipeline::apply(std::shared_ptr<ov::Model> func) {
manager.register_pass<ov::intel_gpu::ConvertFullyConnectedToFullyConnectedCompressed>();
manager.register_pass<ov::intel_gpu::ConvertGatherToGatherCompressed>();
manager.register_pass<ov::intel_gpu::RMSFusion>();
manager.register_pass<ov::intel_gpu::KVCacheFusion>();
// This is supposed to be the last pass to ensure that we don't have name collisions until
// GPU plugin stops using friendly names for program creation
@@ -74,7 +74,8 @@ void VariableState::update_device_buffer() {
if (actual_size < m_layout.bytes_count()) {
const auto alloc_type = m_context->get_engine().use_unified_shared_memory() ? cldnn::allocation_type::usm_device : cldnn::allocation_type::cl_mem;
const auto current_shape = get_tensor_shape(m_layout.get_partial_shape());
const auto current_buf_size = m_layout.get_buffer_size().sizes();
ov::Shape current_shape(current_buf_size.begin(), current_buf_size.end());
const auto alloc_shape = predict_shape(m_name, current_shape, m_layout.data_type, *m_shape_predictor);
const auto alloc_layout = cldnn::layout(alloc_shape, m_layout.data_type, m_layout.format);
m_memory = m_context->get_engine().allocate_memory(alloc_layout, alloc_type, false);
@@ -9,6 +9,9 @@
#include <intel_gpu/primitives/data.hpp>
#include <intel_gpu/primitives/gather.hpp>
#include <intel_gpu/primitives/concatenation.hpp>
#include <intel_gpu/primitives/kv_cache.hpp>
#include <intel_gpu/primitives/read_value.hpp>
#include <intel_gpu/runtime/internal_properties.hpp>
#include "program_wrapper.h"
@@ -126,4 +129,47 @@ TEST(stateful_model, not_skip_gather_at_runtime) {
network.execute();
ASSERT_EQ(gather_inst->can_be_optimized(), false);
}
TEST(stateful_model, check_dynamic_pad_for_kv_cache) {
auto& engine = get_test_engine();
auto input_beam_idx_lay = layout{ov::PartialShape{-1}, data_types::i32, format::bfyx};
auto input_present_lay = layout{ov::PartialShape{-1, 32, -1, 128}, data_types::f32, format::bfyx};
ov::op::util::VariableInfo info{ov::PartialShape{-1, 32, -1, 128}, data_types::f32, "v0"};
auto input_kv_lay = layout{info.data_shape, info.data_type, format::bfyx};
topology topology(input_layout("beam_idx", input_beam_idx_lay),
input_layout("present", input_present_lay),
read_value("kv_cache", std::vector<input_info>{}, info.variable_id, input_kv_lay),
gather("gather",
input_info("kv_cache"),
input_info("beam_idx"),
0, // axis
input_kv_lay.get_partial_shape().size(), // input rank
ov::Shape{}, // output shape
0, // batch_dim
true), // support_neg_ind
kv_cache("concat", {input_info("gather"), input_info("present")}, info, 0, 0),
reorder("reorder", input_info("concat"), format::bfyx, data_types::f32)); /*output padding*/
ExecutionConfig config = get_test_default_config(engine);
config.set_property(ov::intel_gpu::allow_new_shape_infer(true));
config.set_property(ov::intel_gpu::optimize_data(true));
network network(engine, topology, config);
auto gather_inst = network.get_primitive("gather");
auto read_value_inst = network.get_primitive("kv_cache");
auto kv_cache_inst = network.get_primitive("concat");
ASSERT_EQ(gather_inst->get_node().can_be_optimized(), true);
ASSERT_EQ(gather_inst->can_be_optimized(), true);
auto pad = tensor(0);
pad.batch[0] = 1;
ASSERT_EQ(read_value_inst->get_output_layout(0).data_padding.get_dynamic_pad_dims(), pad);
ASSERT_EQ(gather_inst->get_output_layout(0).data_padding.get_dynamic_pad_dims(), pad);
ASSERT_EQ(kv_cache_inst->get_output_layout(0).data_padding.get_dynamic_pad_dims(), pad);
}
} // stateful_model_tests
@@ -0,0 +1,85 @@
// Copyright (C) 2022 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#include "test_utils.h"
#include <intel_gpu/primitives/input_layout.hpp>
#include <intel_gpu/primitives/kv_cache.hpp>
#include <intel_gpu/primitives/data.hpp>
#include "kv_cache_inst.h"
#include "program_wrapper.h"
#include <cmath>
#include <algorithm>
using namespace cldnn;
using namespace ::tests;
namespace shape_infer_tests {
struct kv_cache_test_params {
std::vector<layout> input_layouts;
int64_t concat_axis;
int64_t gather_axis;
layout expected_layout;
};
class kv_cache_test : public testing::TestWithParam<kv_cache_test_params> { };
TEST_P(kv_cache_test, shape_infer) {
auto p = GetParam();
auto& engine = get_test_engine();
cldnn::program prog(engine);
std::vector<std::shared_ptr<input_layout>> input_prims;
std::vector<input_info> input_prims_ids;
for (size_t i = 0; i < p.input_layouts.size(); i++) {
auto prim_id = "data" + std::to_string(i);
auto data_layout_prim = std::make_shared<input_layout>(prim_id, p.input_layouts[i]);
input_prims.push_back(data_layout_prim);
input_prims_ids.push_back(input_info(prim_id));
}
ov::op::util::VariableInfo info{p.input_layouts[0].get_partial_shape(), p.input_layouts[0].data_type, "v0"};
auto kv_cache_prim = std::make_shared<kv_cache>("output", input_prims_ids, info, p.concat_axis, p.gather_axis);
auto& kv_cache_node = prog.get_or_create(kv_cache_prim);
for (size_t i = 0; i < p.input_layouts.size(); i++) {
auto& input_node = prog.get_or_create(input_prims[i]);
program_wrapper::add_connection(prog, input_node, kv_cache_node);
}
auto params = kv_cache_node.get_kernel_impl_params();
auto res = kv_cache_inst::calc_output_layouts<ov::PartialShape>(kv_cache_node, *params);
ASSERT_EQ(res.size(), 1);
ASSERT_EQ(res[0], p.expected_layout);
}
INSTANTIATE_TEST_SUITE_P(smoke, kv_cache_test,
testing::ValuesIn(std::vector<kv_cache_test_params>{
{
{
layout{ov::PartialShape{-1, 2, -1, 4}, data_types::f32, format::bfyx},
layout{ov::PartialShape{-1, 2, -1, 4}, data_types::f32, format::bfyx},
},
2,
0,
layout{ov::PartialShape{-1, 2, -1, 4}, data_types::f32, format::bfyx}
},
{
{
layout{ov::PartialShape{1, 2, 0, 4}, data_types::f16, format::bfyx},
layout{ov::PartialShape{1, 2, 10, 4}, data_types::f16, format::bfyx},
},
2,
0,
layout{ov::PartialShape{1, 2, 10, 4}, data_types::f16, format::bfyx}
},
}));
} // shape_infer_tests
@@ -0,0 +1,78 @@
// Copyright (C) 2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#include <gtest/gtest.h>
#include <string>
#include <memory>
#include <openvino/core/model.hpp>
#include <openvino/pass/manager.hpp>
#include "common_test_utils/ov_test_utils.hpp"
#include <transformations/utils/utils.hpp>
#include <plugin/transformations/kv_cache_fusion.hpp>
#include "intel_gpu/op/read_value.hpp"
#include "openvino/core/node_vector.hpp"
#include "openvino/core/partial_shape.hpp"
#include "openvino/op/parameter.hpp"
#include "openvino/op/result.hpp"
#include "openvino/op/sink.hpp"
#include "intel_gpu/op/kv_cache.hpp"
using namespace testing;
using namespace ov::intel_gpu;
TEST_F(TransformationTestsF, KVCacheFusionTest1) {
{
auto variable = std::make_shared<ov::op::util::Variable>(ov::op::util::VariableInfo{{1, 32, -1, 80}, ov::element::f16, "v0"});
auto past = std::make_shared<ov::op::v6::ReadValue>(variable);
auto parameter = std::make_shared<ov::op::v0::Parameter>(ov::element::f16, ov::PartialShape{1, 32, -1, 80});
auto concat = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{past, parameter}, 2);
auto present = std::make_shared<ov::op::v6::Assign>(concat, variable);
auto result = std::make_shared<ov::op::v0::Result>(concat);
model = std::make_shared<ov::Model>(ov::ResultVector{result}, ov::SinkVector{present}, ov::ParameterVector{parameter});
manager.register_pass<KVCacheFusion>();
}
{
auto variable = std::make_shared<ov::op::util::Variable>(ov::op::util::VariableInfo{{1, 32, -1, 80}, ov::element::f16, "v0"});
auto parameter = std::make_shared<ov::op::v0::Parameter>(ov::element::f16, ov::PartialShape{1, 32, -1, 80});
auto past = std::make_shared<ov::intel_gpu::op::ReadValue>(variable);
auto kv_cache = std::make_shared<ov::intel_gpu::op::KVCache>(past, parameter, variable, 2, ov::element::f16);
auto result = std::make_shared<ov::op::v0::Result>(kv_cache);
model_ref = std::make_shared<ov::Model>(ov::ResultVector{result}, ov::ParameterVector{parameter});
}
}
TEST_F(TransformationTestsF, KVCacheFusionTest2) {
{
auto variable = std::make_shared<ov::op::util::Variable>(ov::op::util::VariableInfo{{1, 32, -1, 80}, ov::element::f32, "v0"});
auto past = std::make_shared<ov::op::v6::ReadValue>(variable);
auto parameter = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape{1, 32, -1, 80});
auto beam_idx = std::make_shared<ov::op::v0::Parameter>(ov::element::i32, ov::PartialShape{1});
auto axis = std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{}, 0);
auto gather_past = std::make_shared<ov::op::v8::Gather>(past, beam_idx, axis);
auto concat = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{gather_past, parameter}, 2);
auto present = std::make_shared<ov::op::v6::Assign>(concat, variable);
auto result = std::make_shared<ov::op::v0::Result>(concat);
model = std::make_shared<ov::Model>(ov::ResultVector{result}, ov::SinkVector{present}, ov::ParameterVector{parameter, beam_idx});
manager.register_pass<KVCacheFusion>();
}
{
auto variable = std::make_shared<ov::op::util::Variable>(ov::op::util::VariableInfo{{1, 32, -1, 80}, ov::element::f32, "v0"});
auto parameter = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape{1, 32, -1, 80});
auto beam_idx = std::make_shared<ov::op::v0::Parameter>(ov::element::i32, ov::PartialShape{1});
auto past = std::make_shared<ov::intel_gpu::op::ReadValue>(variable);
auto axis = std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{}, 0);
auto gather_past = std::make_shared<ov::op::v8::Gather>(past, beam_idx, axis);
auto kv_cache = std::make_shared<ov::intel_gpu::op::KVCache>(gather_past, parameter, variable, 2, ov::element::f32);
auto result = std::make_shared<ov::op::v0::Result>(kv_cache);
model_ref = std::make_shared<ov::Model>(ov::ResultVector{result}, ov::ParameterVector{parameter, beam_idx});
}
}