Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 15 additions & 9 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,19 @@ 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 Either a `std::shared_ptr<GgufDecoder>`, or a path to a file whose extension
/// is `.gguf` and whose first four bytes are the GGUF magic.
/// \return True for either of those; false otherwise.
bool supported_impl(const std::vector<ov::Any>& variants) const override;
Comment thread
mvafin marked this conversation as resolved.

/// \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>.
/// \brief Load the input model, from either of the frontend's two ingest paths.
/// \param variants A single element, holding either:
/// - a `std::shared_ptr<GgufDecoder>` — a decoder supplied by a direct linker, wrapping
/// an already-built ggml graph (the llama.cpp cgraph path); or
/// - a path to a `.gguf` file — parsed here, with the transformer graph built
/// per-architecture by the native builder.
/// Both yield a GgufDecoder, so conversion past this point is identical.
/// \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