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
6 changes: 6 additions & 0 deletions .github/components.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ PROXY:
- GPU
build: []

GGUF_FE:
revalidate:
- CPU # the per-op tests compile and infer their converted graphs on the CPU plugin
build:
- CPU

IR_FE:
revalidate:
- C_API
Expand Down
5 changes: 5 additions & 0 deletions .github/coverage/tests_cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ tests:
mode: gtest_single
profiles: [cpu]

- name: ov_gguf_frontend_tests
binary: ov_gguf_frontend_tests
mode: gtest_single
profiles: [cpu]

- name: ov_inference_functional_tests
binary: ov_inference_functional_tests
mode: gtest_single
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/job_cxx_unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ jobs:
${{ env.INSTALL_TEST_DIR }}/ov_ir_frontend_tests --gtest_print_time=1 --gtest_output=xml:${{ env.INSTALL_TEST_DIR }}/TEST-IRFrontend.xml

- name: GGUF frontend tests
if: fromJSON(inputs.affected-components).GGUF_FE.test
run: |
${{ env.SOURCE_COMMAND }} ${{ env.SETUPVARS }}
${{ env.INSTALL_TEST_DIR }}/ov_gguf_frontend_tests --gtest_print_time=1 --gtest_output=xml:${{ env.INSTALL_TEST_DIR }}/TEST-GGUFFrontend.xml
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ubuntu_24.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ jobs:
build-debian-packages: true
build-contrib: true
build-additional-python-packages: true
generate-gguf-fixtures: true
target-branch: ${{ inputs.target-branch }}
cmake-options: >-
-G 'Ninja Multi-Config'
Expand Down
67 changes: 48 additions & 19 deletions src/frontends/gguf/include/openvino/frontend/gguf/decoder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <string>
#include <vector>

#include "openvino/core/any.hpp"
#include "openvino/core/node.hpp"
#include "openvino/frontend/decoder.hpp"
#include "openvino/frontend/gguf/visibility.hpp"
Expand Down Expand Up @@ -47,17 +48,17 @@ struct RopeConfig {
// Following the established OpenVINO frontend pattern (cf. the PyTorch TorchDecoder + InputModel),
// the translators see a GgufDecoder as a NODE decoder: visit_subgraph hands the visitor a fresh
// decoder bound to a single node, and every per-node accessor (get_attribute, get_input_*,
// get_output_*, get_op_*) refers to that node -- no node index is threaded through. The
// MODEL-level questions (the graph's Parameter inputs, its output names, the shared RoPE config,
// and node iteration) are asked through ov::frontend::gguf::InputModel, not by treating a decoder
// instance as a "model decoder". The InputModel forwards those to the model-scope accessors below;
// a concrete decoder answers them when queried before visit_subgraph binds it to a node.
// get_output_*) refers to that node -- no node index is threaded through. The MODEL-level
// questions (the graph's Parameter inputs, its output names, the shared RoPE config, and node
// iteration) are asked through ov::frontend::gguf::InputModel, not by treating a decoder instance
// as a "model decoder". The InputModel forwards those to the model-scope accessors below; a
// concrete decoder answers them when queried before visit_subgraph binds it to a node.
//
// This is a typed, ggml-free interface: operation parameters are exposed through
// get_attribute(name) / get_input_view_element_offset / get_output_shape / RopeConfig rather than
// raw ggml `op_params` int32 arrays. A concrete decoder (e.g. the llama.cpp cgraph decoder) only
// has to translate ggml's layout into these typed accessors -- the op translators never touch
// ggml memory.
// raw ggml `op_params` int32 arrays. A concrete decoder (e.g. the llama.cpp cgraph decoder, or the
// native .gguf builder decoder) only has to translate ggml's layout into these typed accessors --
// the op translators never touch ggml memory.
class GGUF_FRONTEND_API GgufDecoder : public DecoderBase {
public:
// ── Node scope (the bound node; used by the op translators) ──────────────────────────────
Expand Down Expand Up @@ -104,26 +105,54 @@ class GGUF_FRONTEND_API GgufDecoder : public DecoderBase {
// each node. This is the bridge from model scope to node scope.
virtual void visit_subgraph(std::function<void(std::shared_ptr<GgufDecoder>)> node_visitor) const = 0;

// All model-scope input nodes: both primary inputs (Parameters) and auxiliary inputs
// (position IDs, KV-cache lengths, masks, etc.). Parameters are distinguished from auxiliary
// nodes by the caller via dynamic_pointer_cast<ov::op::v0::Parameter>.
// All model-scope primary input nodes (Parameters): the token/embedding input plus, on the
// stateful path, the KV-cache Parameters. Distinguished from auxiliary nodes by the caller via
// dynamic_pointer_cast<ov::op::v0::Parameter>.
virtual const std::map<std::string, std::shared_ptr<ov::Node>>& get_model_inputs() const = 0;

virtual std::vector<std::string> get_model_output_names() const = 0;

// NOTE: there is no get_model_weights(). A GGUF weight is surfaced as a regular node in
// visit_subgraph with the genuine ggml leaf op type "GGML_OP_NONE": the decoder marks it as a
// weight by exposing the raw weight bytes via get_attribute<ov::Tensor>("data"), the ggml
// quant type name via get_attribute<std::string>("quant_type") (e.g. "Q4_K", "F16") and the
// logical [rows, cols] shape via get_output_shape(). The frontend's translate_weight does the
// dequant / repacking / requantization, so the decoder never builds OV nodes itself. (Model
// inputs are also GGML_OP_NONE leaves, but they are returned via get_model_inputs() and
// resolved to Parameters before the walk, so they carry no "data".)
// ── Optional model scope ───────────────────────────────────────────────────────────────────
//
// The accessors below are how a decoder OPTIONALLY enriches the graph; each has a
// do-nothing default so a decoder only implements what it actually knows. That is what lets
// two very different decoders satisfy one interface: the native .gguf builder answers all of
// them, while the llama.cpp cgraph decoder (which is handed an already-built ggml graph and no
// GGUF metadata) answers none and is not forced to write empty stubs.
//
// Note what is NOT here: nothing describes the execution mode. There is no is_stateful /
// is_static, because a decoder describes ggml OPERATIONS, not a deployment. Conversion always
// yields a stateless graph; a caller that wants an OpenVINO KV cache registers
// ov::frontend::gguf::pass::MakeStateful as a DecoderTransformationExtension.

// Auxiliary model-scope inputs (position IDs, KV-cache lengths, attention masks). A decoder that
// folds these into get_model_inputs() leaves this empty. Note that beam_idx is not among them:
// it is a beam-search index into an OpenVINO state, which ggml has no counterpart for, so
// MakeStateful creates it rather than any decoder declaring it.
virtual const std::map<std::string, std::shared_ptr<ov::Node>>& get_model_extra_inputs() const {
return empty_node_map();
}

// RoPE configuration, exposed through get_attribute<RopeConfig>("rope_config"):
// - at model scope (via InputModel::get_rope_config), used by TranslateSession::preprocess
// to pre-build the shared rope sin/cos table (skipped when RopeConfig::n_dims == 0, i.e.
// no RoPE, or per_op == true);
// - at node scope, the ROPE translator reads the same key for the op's own config.
//
// NOTE: weights are surfaced as GGML_OP_NONE leaves, by every decoder -- there is no separate
// weight accessor. A decoder marks such a leaf either with the raw ggml bytes
// (get_attribute<ov::Tensor>("data") + get_attribute<std::string>("quant_type") +
// get_output_shape(), the llama.cpp cgraph path) or with already-extracted weight/scales/zp
// tensors (get_attribute<bool>("gguf_weight") + "gguf.blob.<sub>" + "gguf_qtype", the native
// .gguf builder path). translate_weight accepts both payloads and builds the same compressed
// decompression subgraph from either.

protected:
// Shared empty map backing the optional accessors above, which return by const reference.
static const std::map<std::string, std::shared_ptr<ov::Node>>& empty_node_map() {
static const std::map<std::string, std::shared_ptr<ov::Node>> empty;
return empty;
}
};

} // namespace ov::frontend::gguf
20 changes: 12 additions & 8 deletions src/frontends/gguf/include/openvino/frontend/gguf/frontend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class GGUF_FRONTEND_API FrontEnd : public ov::frontend::FrontEnd {
/// - `ov::frontend::ConversionExtension` — registers a custom op translator for the
/// ggml op name given by `get_op_type()`. The converter receives an
/// `ov::frontend::gguf::NodeContext` and returns an `ov::OutputVector`.
/// - `ov::frontend::DecoderTransformationExtension` — registers a normalization pass, run
/// AHEAD of the frontend's built-in lowerings. This is how the execution mode is chosen: the
/// frontend always converts to a stateless graph, and a caller that wants an OpenVINO KV
/// cache registers `ov::frontend::gguf::pass::MakeStateful` (or its own variant) here.
/// - `ov::frontend::TelemetryExtension` — receives error / event callbacks.
/// - `ov::detail::SOExtension` — shared-library extension; its inner extension is
/// recursively registered.
Expand All @@ -44,17 +48,17 @@ class GGUF_FRONTEND_API FrontEnd : public ov::frontend::FrontEnd {
void add_extension(const std::shared_ptr<ov::Extension>& extension) override;

protected:
/// \brief Check if FrontEnd can recognize model from given parts.
/// \note Always returns false: this frontend is hidden from FrontEndManager and is never
/// auto-selected. It is used only via direct linkage, by constructing FrontEnd and
/// calling convert() on an InputModel built from a GgufDecoder.
/// \param variants Unused.
/// \return Always false.
/// \brief Check if FrontEnd can recognize the model from the given parts.
/// \param variants A single element holding a `std::shared_ptr<GgufDecoder>`. No other variant
/// is recognized in this frontend: file-path (`.gguf`) loading is not yet implemented.
/// \return True iff variants holds exactly that; false otherwise.
bool supported_impl(const std::vector<ov::Any>& variants) const override;

/// \brief Load the input model from a GgufDecoder.
/// \param variants A single GgufDecoder (a .gguf file path is not accepted; the caller supplies
/// the decoder). variants[0] must hold a std::shared_ptr<GgufDecoder>.
/// \param variants A single element holding a `std::shared_ptr<GgufDecoder>` -- a decoder
/// supplied by a direct linker, wrapping an already-built ggml graph (the llama.cpp
/// cgraph path). File-path (`.gguf`) loading, built per-architecture by the native
/// builder, is not yet implemented in this frontend.
/// \return InputModel::Ptr
InputModel::Ptr load_impl(const std::vector<ov::Any>& variants) const override;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//

#pragma once

#include <memory>
#include <set>
#include <string>

#include "openvino/frontend/gguf/visibility.hpp"
#include "openvino/pass/pass.hpp"

namespace ov::frontend::gguf::pass {



class GGUF_FRONTEND_API MakeStateful : public ov::pass::ModelPass {
public:
OPENVINO_MODEL_PASS_RTTI("gguf::MakeStateful");

/// \param skip_caches Friendly names of cache Parameters to leave stateless. A sliding-window
/// cache is evicted from the front rather than only appended to, so an append-grown
/// Variable would not reproduce it; such caches keep the stateless form.
/// \param append_axis Cache axis the new rows are appended along (the token axis). -1 infers it
/// as the cache Parameter's single dynamic axis, which is how a graph that does not
/// preallocate the cache states its token axis. Pass an explicit axis for a fully static
/// (preallocated) cache, where there is nothing to infer from.
/// \param beam_idx_name Name of the beam-reorder input, which this pass ADDS to the model (it
/// belongs to the state, so no decoder declares it; see the note above). The past cache is
/// gathered by it along the batch axis before the append Concat. With batch 1 /
/// beam_idx [0] that Gather is an identity, but emitting it is what lets CPU's
/// stateful_sdpa_fusion match, and it is what makes beam search work. A model that
/// already carries a Parameter of this name has it reused instead.
explicit MakeStateful(std::set<std::string> skip_caches = {},
int64_t append_axis = -1,
std::string beam_idx_name = "beam_idx")
: m_skip_caches(std::move(skip_caches)),
m_append_axis(append_axis),
m_beam_idx_name(std::move(beam_idx_name)) {}

bool run_on_model(const std::shared_ptr<ov::Model>& model) override;

private:
std::set<std::string> m_skip_caches;
int64_t m_append_axis;
std::string m_beam_idx_name;
};

} // namespace ov::frontend::gguf::pass
7 changes: 3 additions & 4 deletions src/frontends/gguf/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
# SPDX-License-Identifier: Apache-2.0
#

# LINKABLE_FRONTEND: installed alongside the other frontends so callers (the llama.cpp
# ggml-openvino backend, OpenVINO GenAI) can link openvino::frontend::gguf directly. It is kept
# out of the generic FrontEndManager loading API (FrontEndManager treats "gguf" as hidden); see
# the discoverability note in frontend.cpp.
# The GGUF frontend converts GGUF models to OpenVINO. It installs the library + headers alongside
# the other frontends (LINKABLE_FRONTEND, no SKIP_INSTALL) so a direct linker -- the llama.cpp
# ggml-openvino backend -- can link openvino::frontend::gguf and feed it a live GgufDecoder.
ov_add_frontend(NAME gguf
LINKABLE_FRONTEND
FILEDESCRIPTION "FrontEnd to convert GGUF models"
Expand Down
33 changes: 17 additions & 16 deletions src/frontends/gguf/src/frontend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "openvino/frontend/gguf/frontend.hpp"


#include "input_model.hpp"
#include "op_table.hpp"
#include "openvino/core/so_extension.hpp"
Expand All @@ -18,12 +19,17 @@ namespace ov {
namespace frontend {
namespace gguf {

// Discoverability (intentional): consumed only by direct linkage -- a caller (the llama.cpp
// ggml-openvino backend, OpenVINO GenAI) links openvino::frontend::gguf and feeds FrontEnd a live
// GgufDecoder (no .gguf-path reader; see supported_impl / load_impl). It exports the standard
// plugin entry points so FrontEndManager can scan the frontend dir without error, but "gguf" is
// treated as hidden there (manager.cpp is_hidden_frontend), so it is never listed or auto-selected.
// Drop it from that list once this frontend gains file-based loading and passes production review.
// The frontend converts a live GgufDecoder -- supplied by a direct linker, currently the llama.cpp
// ggml-openvino backend -- through the op translators below.
//
// Discoverability: "gguf" is in manager.cpp's is_hidden_frontend list, so it is not advertised by
// available_front_ends() and not auto-selected by load_by_model -- core.read_model(".gguf") does
// not resolve to it. It is still reachable explicitly, either by direct linkage (what GenAI and
// the llama.cpp backend do) or by name via load_by_framework("gguf"). supported_impl below stays
// implemented, so enabling core.read_model later is just dropping the name from that list.
//
// Driving the frontend directly needs no follow-up pass: normalization runs inside convert(), and
// the only step read_model adds, update_v10_model(), fires solely for legacy IR v10.

struct FrontEnd::Impl {
std::unordered_map<std::string, CreatorFunction> op_extension_translators;
Expand Down Expand Up @@ -91,17 +97,14 @@ void FrontEnd::add_extension(const std::shared_ptr<ov::Extension>& extension) {
}
}

bool FrontEnd::supported_impl(const std::vector<ov::Any>&) const {
// Always false: this frontend is never selected by FrontEndManager (load_by_model). It is used
// only via direct linkage -- a caller constructs FrontEnd and calls convert() with an
// InputModel built from a GgufDecoder -- which does not go through supported(). See the
// discoverability note at the top of this file.
return false;
bool FrontEnd::supported_impl(const std::vector<ov::Any>& variants) const {
return !variants.empty() && variants[0].is<std::shared_ptr<GgufDecoder>>();
}

InputModel::Ptr FrontEnd::load_impl(const std::vector<ov::Any>& variants) const {
FRONT_END_GENERAL_CHECK(!variants.empty(),
"GGUF Frontend requires at least one parameter in model representation.");

FRONT_END_GENERAL_CHECK(variants[0].is<std::shared_ptr<GgufDecoder>>(),
"GGUF Frontend supports loading from a GgufDecoder only.");
auto decoder = variants[0].as<std::shared_ptr<GgufDecoder>>();
Expand All @@ -113,10 +116,8 @@ InputModel::Ptr FrontEnd::load_impl(const std::vector<ov::Any>& variants) const
} // namespace frontend
} // namespace ov

// Plugin registration. The frontend is installed in the frontend directory, so it must export
// these entry points or FrontEndManager throws while scanning it. It registers as hidden (see the
// discoverability note above): FrontEndManager loads it without error but never lists or
// auto-selects it; only direct linkers use it.
// Plugin registration. Exports the standard entry points so FrontEndManager can load the library;
// selection is covered by the discoverability note at the top of this file.
GGUF_FRONTEND_C_API ov::frontend::FrontEndVersion get_api_version() {
return OV_FRONTEND_API_VERSION;
}
Expand Down
4 changes: 4 additions & 0 deletions src/frontends/gguf/src/input_model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ void InputModel::visit_subgraph(const std::function<void(std::shared_ptr<GgufDec
m_decoder->visit_subgraph(node_visitor);
}

const std::shared_ptr<GgufDecoder>& InputModel::get_model_decoder() const {
return m_decoder;
}

} // namespace gguf
} // namespace frontend
} // namespace ov
7 changes: 6 additions & 1 deletion src/frontends/gguf/src/input_model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
#include <functional>
#include <map>
#include <memory>
#include "openvino/frontend/input_model.hpp"
#include <string>
#include <vector>

#include "openvino/frontend/gguf/decoder.hpp"
#include "openvino/frontend/gguf/visibility.hpp"
#include "openvino/frontend/input_model.hpp"

namespace ov::frontend::gguf {

Expand All @@ -36,6 +36,11 @@ class GGUF_FRONTEND_API InputModel : public ov::frontend::InputModel {
RopeConfig get_rope_config() const;
void visit_subgraph(const std::function<void(std::shared_ptr<GgufDecoder>)>& node_visitor) const;

// The underlying node-scoped decoder. TranslateSession uses it for the remaining model-scope
// questions that are only relevant on the native .gguf builder / stateful path (weights,
// extra inputs, KV param/result pairs, is_stateful / is_static, tokenizer metadata).
const std::shared_ptr<GgufDecoder>& get_model_decoder() const;

private:
std::shared_ptr<GgufDecoder> m_decoder;
};
Expand Down
Loading
Loading