Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
#include "intel_gpu/primitives/reorder.hpp"
#include "intel_gpu/primitives/primitive.hpp"

#include "fused_shape_utils.hpp"

#include "openvino/core/shape.hpp"

#include "kernel_selector_params.h"
#include "weight_bias_params.h"
#include "kernel_selector_common.h"
Expand Down Expand Up @@ -269,13 +273,30 @@ inline kernel_impl_params canonicalize_fused_shapes(const kernel_impl_params& im
for (auto& fd : updated_impl_params.fused_desc) {
if (fd.is_type<eltwise>() && fd.total_num_deps == 2 && fd.has_outer_dep()) {
if (updated_impl_params.input_layouts.size() > size_t(fd.outer_dep_start_idx)) {
const auto& out_pshape = updated_impl_params.output_layouts[0].get_partial_shape();
const auto& out_layout = updated_impl_params.output_layouts[0];
const auto& out_pshape = out_layout.get_partial_shape();

auto& dep_layout = updated_impl_params.input_layouts[fd.outer_dep_start_idx];
const auto& dep_shape = dep_layout.get_partial_shape();

if (!broadcastable(dep_shape, out_pshape, use_new_shape_infer)) {
dep_layout.set_partial_shape(extend_shape_to_rank_from_begin(dep_shape, out_pshape.size()));
if (dep_shape.size() > out_pshape.size()) {
// extend_shape_to_rank_from_begin() can only prepend unit dims; it leaves a
// higher-rank peer unchanged, so the fused-op kernel would read the peer with a
// rank/format inconsistent with the host iteration space (df1 output-0 defect).
// Fold the peer onto the host rank when it is a provably order-preserving reshape
// (equal-total planar reshape or a legal planar broadcast); otherwise leave it to
// the pre-existing rank-extension path. Optimization inapplicability is not an
// error, so no assertion is raised and valid models always compile.
if (auto folded = fold_higher_rank_fused_peer(dep_layout, out_layout)) {
dep_layout.set_partial_shape(*folded);
dep_layout.format = format::adjust_to_rank(dep_layout.format, out_pshape.size());
} else {
dep_layout.set_partial_shape(extend_shape_to_rank_from_begin(dep_shape, out_pshape.size()));
}
} else {
dep_layout.set_partial_shape(extend_shape_to_rank_from_begin(dep_shape, out_pshape.size()));
}
}
}
}
Expand Down
71 changes: 71 additions & 0 deletions src/plugins/intel_gpu/src/graph/include/fused_shape_utils.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//

#pragma once

#include <optional>

#include "intel_gpu/runtime/layout.hpp"
#include "openvino/core/shape_util.hpp"

namespace cldnn {

// Computes a lower-rank representation of a fused eltwise peer when collapsing its leading spatial
// axes is an order-preserving reshape and the result broadcasts directly to the host layout.
inline std::optional<ov::PartialShape> fold_higher_rank_fused_peer(const layout& peer_layout, const layout& host_layout) {
const auto& peer_shape = peer_layout.get_partial_shape();
const auto& host_shape = host_layout.get_partial_shape();

const size_t peer_rank = peer_shape.size();
const size_t host_rank = host_shape.size();
if (peer_rank <= host_rank || host_rank < 3)
return std::nullopt;
if (peer_shape.is_dynamic() || host_shape.is_dynamic())
return std::nullopt;
if (peer_layout.data_padding || host_layout.data_padding)
return std::nullopt;

const auto& peer_format = peer_layout.format;
const auto& host_format = host_layout.format;
if (!format::is_simple_data_format(peer_format) || !format::is_simple_data_format(host_format))
return std::nullopt;
if (!format::is_default_format(peer_format) || !format::is_default_format(host_format))
return std::nullopt;
if (format::adjust_to_rank(peer_format, host_rank) != host_format)
return std::nullopt;

const auto peer_dims = peer_shape.to_shape();
const auto host_dims = host_shape.to_shape();
const size_t fold_count = peer_rank - host_rank + 1;
ov::Shape folded_dims;
folded_dims.reserve(host_rank);
folded_dims.push_back(peer_dims[0]);
folded_dims.push_back(peer_dims[1]);

size_t grouped = 1;
for (size_t i = 2; i < 2 + fold_count; ++i) {
const auto grouped_size = ov::util::shape_size_safe({grouped, peer_dims[i]});
if (!grouped_size.has_value())
return std::nullopt;
grouped = grouped_size.value();
}
folded_dims.push_back(grouped);
folded_dims.insert(folded_dims.end(), peer_dims.begin() + 2 + fold_count, peer_dims.end());

const auto peer_total = ov::util::shape_size_safe(peer_dims);
const auto folded_total = ov::util::shape_size_safe(folded_dims);
if (!peer_total.has_value() || !folded_total.has_value() || peer_total.value() != folded_total.value())
return std::nullopt;
if (folded_dims.size() != host_dims.size())
return std::nullopt;

for (size_t i = 0; i < folded_dims.size(); ++i) {
if (folded_dims[i] != 1 && folded_dims[i] != host_dims[i])
return std::nullopt;
}

return ov::PartialShape(folded_dims);
}

} // namespace cldnn
43 changes: 41 additions & 2 deletions src/plugins/intel_gpu/src/graph/layout_optimizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
#include "lstm_seq_inst.h"
#include "group_normalization_inst.h"
#include "to_string_utils.h"
#include "fused_shape_utils.hpp"
#include <vector>
#include <memory>
#include <utility>
Expand All @@ -69,6 +70,37 @@ static size_t get_post_ops_count(const program_node& node) {
return onednn_post_ops_count;
}

// A rank-reducing reorder may be fused into a producer only when every higher-rank external eltwise
// peer has the same provable lower-rank representation that OCL fused-op canonicalization will use.
static bool fused_peers_can_fold_to_layout(const program_node& prev, const layout& reduced_layout) {
if (!prev.has_fused_primitives())
return true;

const size_t reduced_rank = reduced_layout.get_rank();
if (prev.get_output_layout().get_rank() <= reduced_rank)
return true;

for (const auto& fd : prev.get_fused_primitives()) {
if (!fd.is_type<eltwise>() || !fd.has_outer_dep())
continue;

const auto outer_idx = static_cast<size_t>(fd.outer_dep_start_idx);
const size_t outer_count = fd.deps.size();
if (outer_idx >= prev.get_dependencies().size() || outer_count == 0 || outer_count > prev.get_dependencies().size() - outer_idx) {
return false;
}

for (size_t i = 0; i < outer_count; ++i) {
const auto peer_layout = prev.get_dependency(outer_idx + i).get_output_layout();
if (peer_layout.get_rank() > reduced_rank && !fold_higher_rank_fused_peer(peer_layout, reduced_layout).has_value()) {
return false;
}
}
}

return true;
}

std::pair<std::shared_ptr<reorder>, bool> reorder_factory::get_reorder(primitive_id src_id,
int32_t src_port,
const layout& in_layout,
Expand Down Expand Up @@ -391,15 +423,22 @@ bool layout_optimizer::can_fuse_reorder(program_node& prev, program_node& next,

bool layout_optimizer::can_fuse_reorder_to_prev(program_node& prev, reorder_node& node, format fmt_prev, format fmt_next) {
bool allow_new_shape_infer = node.get_program().is_new_shape_infer();

// Do not fuse a rank-reducing reorder into a producer whose fused higher-rank
// eltwise peer cannot be indexed correctly at the reduced rank (inner-spatial
// broadcast over the collapsed axes). Keeping the producer at its higher rank
// keeps the fused peer rank-consistent so the fused-op kernel reads it right.
if (!node.get_output_layout().is_dynamic() && !fused_peers_can_fold_to_layout(prev, node.get_output_layout())) {
return false;
}
// Because kernels can work cross-layout, if reorder only performs type conversion,
// fusing reorder to the previous node can be done even if it is a dynamic shape case
if ((format::is_simple_data_format(fmt_prev) && format::is_simple_data_format(fmt_next)) &&
// If the prev node is backedge of the loop, the type will be changed by fusing reorder.
// We can void only that case if we can check whether the current node is backedge of the network.
// However no such handle is existing yet. (To be done in the future when we need to optimize out the type converting
// reorders in the body network)
!node.get_program().is_body_program() && !prev.is_in_shape_of_subgraph() &&
prev.get_preferred_impl_type() != cldnn::impl_types::cpu) {
!node.get_program().is_body_program() && !prev.is_in_shape_of_subgraph() && prev.get_preferred_impl_type() != cldnn::impl_types::cpu) {
// case for truncate mode
if ((prev.is_type<mvn>() || prev.is_type<concatenation>() || prev.is_type<gather>() || prev.is_type<broadcast>() ||
prev.is_type<select>() || prev.is_type<eltwise>() || prev.is_type<rms>()) &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//

#include "move_eltwise_up_data_movement_per_channel.hpp"

#include <algorithm>
#include <cstdint>
#include <limits>
#include <memory>

#include "openvino/core/graph_util.hpp"
#include "openvino/core/rt_info.hpp"
#include "openvino/op/constant.hpp"
#include "openvino/op/reshape.hpp"
#include "openvino/op/squeeze.hpp"
#include "openvino/op/unsqueeze.hpp"
#include "openvino/op/util/binary_elementwise_arithmetic.hpp"
#include "openvino/pass/pattern/op/wrap_type.hpp"

namespace {

using ov::pass::pattern::Matcher;
using ov::pass::pattern::wrap_type;

namespace v0 = ov::op::v0;
namespace v1 = ov::op::v1;
namespace op_util = ov::op::util;

} // namespace

ov::intel_gpu::MoveEltwiseUpThroughDataMovPerChannel::MoveEltwiseUpThroughDataMovPerChannel() {
auto const_predicate = [](const ov::Output<ov::Node>& output) {
auto constant_op = ov::as_type_ptr<v0::Constant>(output.get_node_shared_ptr());
if (!constant_op)
return false;

if (output.get_target_inputs().size() != 1)
return false;

const auto& shape = constant_op->get_shape();
return std::count_if(shape.begin(), shape.end(), [](size_t value) {
return value > 1;
}) == 1;
};

auto eltw_predicate = [](const ov::Output<ov::Node>& output) {
if (output.get_target_inputs().size() != 1)
return false;

auto node = output.get_node();
if (node->get_output_partial_shape(0).rank().is_dynamic())
return false;

const size_t const_idx = ov::is_type<v0::Constant>(node->get_input_node_ptr(0)) ? 0 : 1;
const size_t data_flow_idx = (const_idx + 1) % 2;

return node->get_input_partial_shape(data_flow_idx).size() >= node->get_input_partial_shape(const_idx).size();
};

auto eltw_data_flow_in = wrap_type<v1::Reshape, v0::Squeeze, v0::Unsqueeze>(ov::pass::pattern::consumers_count(1));
auto eltw_const_in = wrap_type<v0::Constant>(const_predicate);
auto eltwise_pattern = wrap_type<op_util::BinaryElementwiseArithmetic>({eltw_data_flow_in, eltw_const_in}, eltw_predicate);

ov::matcher_pass_callback callback = [this, eltwise_pattern](Matcher& matcher) {
const auto& pattern_map = matcher.get_pattern_value_map();

auto eltwise = pattern_map.at(eltwise_pattern).get_node_shared_ptr();
if (transformation_callback(eltwise))
return false;

auto binary_eltwise = std::dynamic_pointer_cast<op_util::BinaryElementwiseArithmetic>(eltwise);
if (!binary_eltwise || binary_eltwise->get_autob().m_type != ov::op::AutoBroadcastType::NUMPY)
return false;

const size_t const_idx = ov::is_type<v0::Constant>(eltwise->get_input_node_ptr(0)) ? 0 : 1;
const size_t data_flow_idx = (const_idx + 1) % 2;

const auto const_shape = eltwise->get_input_shape(const_idx);
size_t channel_idx = 0;
size_t channel_val = 0;
for (size_t index = 0; index < const_shape.size(); ++index) {
if (const_shape[index] > 1) {
channel_idx = index;
channel_val = const_shape[index];
}
}

auto parent = eltwise->get_input_node_shared_ptr(data_flow_idx);
const auto& parent_in_pshape = parent->get_input_partial_shape(0);
const auto& parent_out_pshape = parent->get_output_partial_shape(0);

if (parent_in_pshape.rank().is_dynamic() || parent_out_pshape.rank().is_dynamic())
return false;

const size_t in_rank = parent_in_pshape.size();
const size_t out_rank = parent_out_pshape.size();

for (const auto& dimension : parent_in_pshape) {
if (dimension.is_dynamic())
return false;
}
for (const auto& dimension : parent_out_pshape) {
if (dimension.is_dynamic())
return false;
}

const size_t const_rank = const_shape.size();
if (const_rank > out_rank)
return false;

const size_t output_channel_idx = out_rank - const_rank + channel_idx;
if (output_channel_idx >= out_rank || static_cast<size_t>(parent_out_pshape[output_channel_idx].get_length()) != channel_val) {
return false;
}

int64_t output_stride = 1;
for (size_t index = output_channel_idx + 1; index < out_rank; ++index) {
const int64_t dimension = parent_out_pshape[index].get_length();
if (dimension > 0 && output_stride > std::numeric_limits<int64_t>::max() / dimension)
return false;
output_stride *= dimension;
}

size_t input_channel_idx = in_rank;
for (size_t index = 0; index < in_rank; ++index) {
if (static_cast<size_t>(parent_in_pshape[index].get_length()) != channel_val)
continue;

int64_t input_stride = 1;
bool overflow = false;
for (size_t trailing_index = index + 1; trailing_index < in_rank; ++trailing_index) {
const int64_t dimension = parent_in_pshape[trailing_index].get_length();
if (dimension > 0 && input_stride > std::numeric_limits<int64_t>::max() / dimension) {
overflow = true;
break;
}
input_stride *= dimension;
}

if (overflow)
continue;

if (input_stride == output_stride) {
if (input_channel_idx != in_rank)
return false;
input_channel_idx = index;
}
}

if (input_channel_idx == in_rank)
return false;

auto new_shape = ov::Shape(in_rank, 1);
new_shape[input_channel_idx] = channel_val;
auto old_constant = ov::as_type_ptr<v0::Constant>(eltwise->get_input_node_shared_ptr(const_idx));
auto new_constant = std::make_shared<v0::Constant>(*old_constant, new_shape);
ov::replace_node_update_name(old_constant, new_constant);
ov::replace_output_update_name(eltwise->output(0), eltwise->input_value(data_flow_idx));

ov::OutputVector eltwise_inputs = eltwise->input_values();
eltwise_inputs[data_flow_idx] = parent->input_value(0);
auto new_eltwise = eltwise->clone_with_new_inputs(eltwise_inputs);
ov::copy_runtime_info(eltwise, new_eltwise);

ov::OutputVector parent_inputs = parent->input_values();
parent_inputs[0] = new_eltwise;
auto new_parent = parent->clone_with_new_inputs(parent_inputs);
ov::copy_runtime_info(parent, new_parent);
new_parent->set_friendly_name(parent->get_friendly_name());

ov::replace_node(parent, new_parent);
return true;
};

auto matcher = std::make_shared<Matcher>(eltwise_pattern, "MoveEltwiseUpThroughDataMovPerChannelGPU");
register_matcher(matcher, callback);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//

#pragma once

#include "openvino/pass/graph_rewrite.hpp"

namespace ov::intel_gpu {

class MoveEltwiseUpThroughDataMovPerChannel : public ov::pass::MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("MoveEltwiseUpThroughDataMovPerChannelGPU");
MoveEltwiseUpThroughDataMovPerChannel();
};

} // namespace ov::intel_gpu
Loading
Loading