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
10 changes: 7 additions & 3 deletions ggml/src/ggml-openvino/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
find_package(OpenVINO REQUIRED COMPONENTS Runtime Threading)
find_package(OpenCL REQUIRED)

if(NOT TARGET openvino::frontend::gguf)
message(FATAL_ERROR "OpenVINO GGUF frontend target openvino::frontend::gguf is required")
endif()

message(STATUS "Found OpenVINO: ${OpenVINO_DIR} (found version \"${OpenVINO_VERSION}\")")

file(GLOB_RECURSE GGML_HEADERS_OPENVINO "*.h" "*.hpp")
file(GLOB_RECURSE GGML_SOURCES_OPENVINO "*.cpp")
file(GLOB GGML_HEADERS_OPENVINO "*.h" "*.hpp")
file(GLOB GGML_SOURCES_OPENVINO "*.cpp")

ggml_add_backend_library(ggml-openvino
${GGML_SOURCES_OPENVINO}
${GGML_HEADERS_OPENVINO}
)

target_link_libraries(ggml-openvino PRIVATE openvino::runtime openvino::threading OpenCL::OpenCL)
target_link_libraries(ggml-openvino PRIVATE openvino::runtime openvino::threading openvino::frontend::gguf OpenCL::OpenCL)

# the OpenVINO RTTI macros take one argument and leave __VA_ARGS__ empty, which -Wpedantic reports
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "IntelLLVM")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
#include "align_eltwise_ranks.h"
#include "align-eltwise-ranks.h"

#include <numeric>
#include <openvino/op/add.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/power.hpp>
#include <openvino/op/sqrt.hpp>
#include <openvino/op/subtract.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <openvino/pass/pattern/op/wrap_type.hpp>

namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
namespace ggml_openvino {

namespace {

// True for an RMS-norm output, x * (1 / sqrt(mean(x^2) + eps)) as translate_rms_norm builds it, optionally
// scaled by the norm weight.
// True for an RMS-norm output, x * (1 / sqrt(mean(x^2) + eps)) or x * pow(mean(x^2) + eps, -0.5), optionally scaled
// by the norm weight.
bool is_rms_norm_output(const ov::Output<ov::Node> & value, int depth = 1) {
const auto * node = value.get_node();
if (!ov::is_type<ov::op::v1::Multiply>(node)) {
Expand All @@ -29,6 +27,9 @@ bool is_rms_norm_output(const ov::Output<ov::Node> & value, int depth = 1) {
if (ov::is_type<ov::op::v1::Divide>(src) && ov::is_type<ov::op::v0::Sqrt>(src->get_input_node_ptr(1))) {
return true;
}
if (ov::is_type<ov::op::v1::Power>(src) && ov::is_type<ov::op::v0::Constant>(src->get_input_node_ptr(1))) {
return true;
}
if (depth > 0 && is_rms_norm_output(input, depth - 1)) {
return true;
}
Expand All @@ -50,11 +51,9 @@ AlignEltwiseOperandRanks::AlignEltwiseOperandRanks() {
auto lhs = node->input_value(0);
auto rhs = node->input_value(1);

// A tensor-vs-Constant mismatch is the norm's own eps / 1-over-sqrt arithmetic. That
// folds into the `rms` primitive itself instead of becoming a fused eltwise post-op,
// so it is not affected and is left alone.
if (ov::is_type<ov::op::v0::Constant>(lhs.get_node()) ||
ov::is_type<ov::op::v0::Constant>(rhs.get_node())) {
// A tensor-vs-Constant mismatch is the norm's own eps / 1-over-sqrt arithmetic, which folds into the `rms`
// primitive itself instead of becoming a fused eltwise post-op, so it is not affected.
if (ov::is_type<ov::op::v0::Constant>(lhs.get_node()) || ov::is_type<ov::op::v0::Constant>(rhs.get_node())) {
return false;
}

Expand All @@ -67,9 +66,9 @@ AlignEltwiseOperandRanks::AlignEltwiseOperandRanks() {
const size_t shorter_idx = lhs_rank.get_length() < rhs_rank.get_length() ? 0 : 1;
const auto & shorter = shorter_idx == 0 ? lhs : rhs;

// The defect is with the norm output as the higher-rank operand. When the norm output is the
// lower-rank one (gemma-3 adds it to a rank-4 residual), unsqueezing it puts the Unsqueeze between
// `rms` and its post-op, and the GPU plugin then computes the layer wrongly.
// The defect is with the norm output as the higher-rank operand. When the norm output is the lower-rank
// one, unsqueezing it puts the Unsqueeze between `rms` and its post-op, and the plugin then computes the
// layer wrongly.
if (is_rms_norm_output(shorter)) {
return false;
}
Expand All @@ -85,12 +84,8 @@ AlignEltwiseOperandRanks::AlignEltwiseOperandRanks() {
return true;
};

register_matcher(
std::make_shared<ov::pass::pattern::Matcher>(eltwise_m, "ov::frontend::ggml::pass::AlignEltwiseOperandRanks"),
callback);
register_matcher(std::make_shared<ov::pass::pattern::Matcher>(eltwise_m, "ggml_openvino::AlignEltwiseOperandRanks"),
callback);
}

} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
} // namespace ggml_openvino
25 changes: 25 additions & 0 deletions ggml/src/ggml-openvino/align-eltwise-ranks.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#include "openvino/pass/matcher_pass.hpp"

namespace ggml_openvino {

// Gives a binary eltwise op's two operands the same rank, by unsqueezing leading axes onto the shorter one.
//
// This is semantically a no-op (NUMPY broadcasting already left-pads the lower-rank operand with 1s, and the result
// rank is max(rank_a, rank_b) either way). It exists purely to work around an OpenVINO GPU-plugin defect: an eltwise
// op whose operands differ in rank is computed incorrectly once the plugin fuses it as a post-op into an `rms`
// primitive (e.g. a rank-3 residual added to a rank-4 RMS-norm output; the model degenerates into repeated tokens on
// GPU while CPU is correct). Equalising the ranks keeps the fusion and makes it compute the right answer.
//
// Only applies when both operands are real tensors: a tensor-vs-Constant mismatch is the RMS norm's own eps/rsqrt
// arithmetic, which folds into the `rms` primitive and is not affected. The norm output itself is never unsqueezed:
// when it is the lower-rank operand, that breaks the fused path instead. Registered for GPU only, disabled by
// GGML_OPENVINO_DISABLE_ELTWISE_RANK_ALIGN. Remove once the plugin guards that fusion.
class AlignEltwiseOperandRanks : public ov::pass::MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("ggml_openvino::AlignEltwiseOperandRanks")
AlignEltwiseOperandRanks();
};

} // namespace ggml_openvino
Loading