Skip to content

Commit 9c2d652

Browse files
committed
[GGUF FE] Simplify decoder API and refactor stateless/stateful KV cache
Refactor the frontend so conversion is device- and mode-agnostic, trim the decoder interface to what the translators actually consume, drop dead code, and make the frontend installable while hidden from FrontEndManager. Decoder API: - Surface GGUF weights as genuine GGML_OP_NONE leaf nodes (marked by a "data" attribute) instead of a synthetic op. - Make GgufDecoder node-scoped: visit_subgraph binds the decoder to one node and the per-node accessors drop the node_idx parameter. - Fold has_rope / use_per_op_rope / get_rope_config into a single get_attribute("rope_config"); drop is_swa_layer, is_static, is_stateful, get_model_output_shape and get_kv_param_res_names from the interface (they are backend-internal or unused). - Drop unused NodeContext members (translate_session, get_output_name) and the dead, never-registered FuseToSDPA pass (the flash-attn translator emits ScaledDotProductAttention directly, so there is nothing to fuse). - Use the real ov::pass::MarkCompressedFloatConstants transformation (link openvino::offline_transformations) instead of a hand-copied forward-declaration header. - Remove dead code carried over from the initial copy: unused translator declarations (translate_add / translate_mul), unused utils helpers (getCurrentTime, dump_ov_model, non_cont_dim, argsort_descend, sorted_descend, is_permuted, permute), the unused make_fused_qkv_weights / slice_rows weight splitters, the dead stateful branch/parameter of make_sin_cos, and the never-defined get_shape(gguf_tensor) declaration (gguf_fill_mxfp4 now computes rows/cols from tensor.dim directly, fixing a Windows link error). Stateless / stateful KV cache: - The frontend always emits the stateless, llama.cpp-faithful graph. Every ggml SET_ROWS becomes an internal SetRows op, lowered in the normalization stage: the built-in LowerSetRowsStateless rewrites it to ScatterUpdate by default. - A caller (the ggml-openvino backend) may register a stateful lowering via DecoderTransformationExtension to convert the attention KV writes into an OpenVINO stateful ReadValue/Concat/Assign subgraph -- selected structurally, with no decoder-side flag. - The NPU-only SqueezeMatmul pass moves out of the frontend to the backend. Install + discoverability: - Install the frontend library and headers alongside the other frontends (drop SKIP_INSTALL) so callers can link openvino::frontend::gguf directly, and add the GGUF component to the Debian/RPM packaging (copyright, package name). - Add FrontEndPluginInfo::m_hidden: a hidden frontend loads normally but is excluded from FrontEndManager::get_available_front_ends() and from load_by_model / load_by_framework. The GGUF frontend registers hidden (and supported_impl returns false), so it is not exposed via the generic model loading API while remaining usable by direct linkers. Tests: - expect_near uses a combined absolute + relative tolerance so the op tests pass on hardware that runs the graph in fp16 (e.g. ARM CPU by default), where a fixed fp32-tuned absolute tolerance is too tight.
1 parent 3b3815f commit 9c2d652

36 files changed

Lines changed: 460 additions & 758 deletions

cmake/packaging/debian.cmake

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,19 @@ macro(ov_cpack_settings)
327327
set(tensorflow_lite_copyright "generic")
328328
endif()
329329

330+
if(ENABLE_OV_GGUF_FRONTEND)
331+
set(CPACK_COMPONENT_GGUF_DESCRIPTION "OpenVINO GGUF Frontend")
332+
set(CPACK_COMPONENT_GGUF_DEPENDS "${OV_CPACK_COMP_CORE}")
333+
set(CPACK_DEBIAN_GGUF_PACKAGE_NAME "libopenvino-gguf-frontend-${cpack_name_ver}")
334+
# since GGUF FE is a linkable target, we need to call ldconfig (i.e. `def_triggers`)
335+
set(CPACK_DEBIAN_GGUF_PACKAGE_CONTROL_EXTRA "${def_postinst};${def_postrm};${def_triggers}")
336+
ov_debian_add_lintian_suppression(gguf
337+
# we have different package name strategy; it suggests libopenvino-gguf-frontend202230
338+
"package-name-doesnt-match-sonames")
339+
list(APPEND frontends gguf)
340+
set(gguf_copyright "generic")
341+
endif()
342+
330343
#
331344
# core_dev: depends on core and frontends (since frontends don't want to provide its own dev packages)
332345
#

cmake/packaging/rpm.cmake

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,15 @@ macro(ov_cpack_settings)
277277
set(tensorflow_lite_copyright "generic")
278278
endif()
279279

280+
if(ENABLE_OV_GGUF_FRONTEND)
281+
set(CPACK_COMPONENT_GGUF_DESCRIPTION "OpenVINO GGUF Frontend")
282+
set(CPACK_RPM_GGUF_PACKAGE_NAME "libopenvino-gguf-frontend-${cpack_name_ver}")
283+
set(CPACK_RPM_GGUF_POST_INSTALL_SCRIPT_FILE "${def_triggers}")
284+
set(CPACK_RPM_GGUF_POST_UNINSTALL_SCRIPT_FILE "${def_triggers}")
285+
_ov_add_package(frontend_packages gguf)
286+
set(gguf_copyright "generic")
287+
endif()
288+
280289
#
281290
# core_dev: depends on core and frontends (since frontends don't want to provide its own dev packages)
282291
#

src/frontends/common/include/openvino/frontend/manager.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ using FrontEndVersion = uint64_t;
123123
struct FrontEndPluginInfo {
124124
std::string m_name;
125125
FrontEndFactory m_creator;
126+
/// When true, the frontend is hidden from the generic model-loading API: it is not returned
127+
/// by FrontEndManager::get_available_front_ends() and is never auto-selected by
128+
/// load_by_model / load_by_framework. Such a frontend is intended for direct linkage only
129+
/// (the caller constructs it explicitly). It is still installed and loadable, so it must
130+
/// export get_api_version / get_front_end_data like any other plugin.
131+
bool m_hidden = false;
126132
};
127133

128134
} // namespace frontend

src/frontends/common/src/manager.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ class FrontEndManager::Impl {
5858
// Load plugins until we found the right one
5959
for (auto& plugin : m_plugins) {
6060
OPENVINO_ASSERT(plugin.load(), "Cannot load frontend ", plugin.get_name_from_file());
61+
// Hidden frontends are not selectable by name through the generic API.
62+
if (plugin.get_creator().m_hidden) {
63+
continue;
64+
}
6165
if (plugin.get_creator().m_name == framework) {
6266
return make_frontend(plugin);
6367
}
@@ -74,6 +78,10 @@ class FrontEndManager::Impl {
7478
OPENVINO_DEBUG("Frontend load failed: ", plugin_info.m_file_path, "\n");
7579
continue;
7680
}
81+
// Hidden frontends are for direct linkage only; do not advertise them.
82+
if (plugin_info.get_creator().m_hidden) {
83+
continue;
84+
}
7785
names.push_back(plugin_info.get_creator().m_name);
7886
}
7987
return names;
@@ -91,6 +99,10 @@ class FrontEndManager::Impl {
9199
if (!plugin.load()) {
92100
continue;
93101
}
102+
// Hidden frontends are for direct linkage only; never auto-select them.
103+
if (plugin.get_creator().m_hidden) {
104+
continue;
105+
}
94106
auto fe = plugin.get_creator().m_creator();
95107
OPENVINO_ASSERT(fe, "Frontend error: frontend '", plugin.get_creator().m_name, "' created null FrontEnd");
96108
if (fe->supported(variants)) {

src/frontends/gguf/include/openvino/frontend/gguf/decoder.h

Lines changed: 40 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -20,103 +20,93 @@ namespace gguf {
2020
// gguf-file decoder by construction), so the op translators never need to know ggml's
2121
// memory layout.
2222
struct RopeConfig {
23-
int n_dims = 0;
23+
int n_dims = 0; // 0 means the model uses no RoPE (replaces a separate has_rope() query)
2424
int n_ctx_orig = 0;
2525
float freq_base = 0.0f;
2626
float freq_scale = 0.0f;
2727
float ext_factor = 0.0f;
2828
float attn_factor = 0.0f;
2929
float beta_fast = 0.0f;
3030
float beta_slow = 0.0f;
31+
// When true, each ROPE op builds its own sin/cos from its per-op config (e.g. gemma4, where
32+
// SWA and global layers use different n_dims), so the shared rope_cos/rope_sin table that
33+
// TranslateSession::preprocess would otherwise pre-build is skipped.
34+
bool per_op = false;
3135
};
3236

3337
// Decoder interface consumed by the gguf frontend translators.
3438
//
39+
// Following the established OpenVINO frontend pattern (cf. the PyTorch frontend), a GgufDecoder
40+
// is node-scoped: visit_subgraph hands the visitor a fresh decoder bound to a single node, and
41+
// every per-node accessor (get_attribute, get_input_*, get_output_*, get_op_*) refers to that
42+
// node -- no node index is threaded through. The same object type, queried at model scope (the
43+
// instance returned by InputModel::get_decoder and iterated by visit_subgraph), answers the
44+
// model-level questions (get_model_inputs, get_model_output_names, get_rope_config, ...).
45+
//
3546
// This is a typed, ggml-free interface: operation parameters are exposed through
36-
// get_attribute(node_idx, name) / get_input_view_offset / RopeConfig rather than as raw
37-
// ggml `op_params` int32 arrays. A concrete decoder (e.g. the llama.cpp cgraph decoder)
38-
// only has to translate ggml's layout into these typed accessors -- the op translators
39-
// here never touch ggml memory directly.
47+
// get_attribute(name) / get_input_view_offset / RopeConfig rather than as raw ggml `op_params`
48+
// int32 arrays. A concrete decoder (e.g. the llama.cpp cgraph decoder) only has to translate
49+
// ggml's layout into these typed accessors -- the op translators here never touch ggml memory.
4050
class GgufDecoder : public DecoderBase {
4151
public:
52+
// Per-node typed attribute access (the bound node). The op translators use this to read
53+
// scalar operation parameters (e.g. "eps", "scale", "bias", "swapped", "rope_config")
54+
// without dereferencing ggml's raw op_params layout.
4255
ov::Any get_attribute(const std::string& name) const override = 0;
4356

44-
// Per-node typed attribute access. This is the mechanism the op translators use to
45-
// read scalar operation parameters (e.g. "eps", "scale", "bias", "swapped",
46-
// "softmax_axis", "rope_config") without dereferencing ggml's raw op_params layout.
47-
virtual ov::Any get_attribute(int node_idx, const std::string& name) const = 0;
48-
49-
virtual PartialShape get_input_shape(int node_idx, const std::string& name) const = 0;
57+
virtual PartialShape get_input_shape(const std::string& name) const = 0;
5058

51-
virtual std::vector<size_t> get_input_stride(int node_idx, const std::string& name) const = 0;
59+
virtual std::vector<size_t> get_input_stride(const std::string& name) const = 0;
5260

5361
// Byte offset of an input that is a gguf VIEW into a larger tensor (0 when the input
5462
// is not a view). Replaces the raw `get_input_op_params(...)[0]` read: a view's start
5563
// offset is a single semantic scalar the decoder knows (by parsing ggml, or by
5664
// construction), so translators never touch ggml's op_params layout. Translators
5765
// convert bytes -> elements using get_input_stride as needed.
58-
virtual int64_t get_input_view_offset(int node_idx, const std::string& name) const = 0;
66+
virtual int64_t get_input_view_offset(const std::string& name) const = 0;
5967

60-
virtual element::Type get_input_type(int node_idx, const std::string& name) const = 0;
68+
virtual element::Type get_input_type(const std::string& name) const = 0;
6169

6270
size_t get_input_size() const override = 0;
6371

64-
virtual size_t get_input_size(int node_idx) const = 0;
65-
6672
void get_input_node(size_t input_port_idx,
6773
std::string& producer_name,
6874
std::string& producer_output_port_name,
6975
size_t& producer_output_port_index) const override = 0;
7076

71-
virtual std::vector<std::string> get_input_names(int node_idx) const = 0;
77+
virtual std::vector<std::string> get_input_names() const = 0;
7278

73-
virtual PartialShape get_output_shape(int node_idx) const = 0;
79+
virtual PartialShape get_output_shape() const = 0;
7480

75-
virtual element::Type get_output_type(const int node_idx) const = 0;
81+
virtual element::Type get_output_type() const = 0;
7682

77-
virtual std::vector<std::string> get_output_names(int node_idx) const = 0;
83+
virtual std::vector<std::string> get_output_names() const = 0;
7884

7985
const std::string& get_op_type() const override = 0;
8086

81-
virtual const std::string& get_op_type(int node_idx) const = 0;
82-
8387
const std::string& get_op_name() const override = 0;
8488

85-
virtual const std::string& get_op_name(int node_idx) const = 0;
86-
87-
virtual void visit_subgraph(std::function<void(std::shared_ptr<GgufDecoder>, int node_idx)> node_visitor) const = 0;
89+
virtual void visit_subgraph(std::function<void(std::shared_ptr<GgufDecoder>)> node_visitor) const = 0;
8890

89-
virtual int get_op_case(int node_idx) const = 0;
91+
virtual int get_op_case() const = 0;
9092

9193
virtual const std::map<std::string, std::shared_ptr<ov::Node>>& get_model_inputs() const = 0;
9294
virtual const std::map<std::string, std::shared_ptr<ov::Node>>& get_model_extra_inputs() const = 0;
9395
virtual std::vector<std::string> get_model_output_names() const = 0;
9496

9597
// NOTE: there is no get_model_weights(). A GGUF weight is surfaced as a regular node in
96-
// visit_subgraph with op type "GGML_OP_WEIGHT": the decoder exposes the raw weight bytes
97-
// via get_attribute<ov::Tensor>(node_idx, "data"), the ggml quant type name via
98-
// get_attribute<std::string>(node_idx, "quant_type") (e.g. "Q4_K", "F16") and the logical
99-
// [rows, cols] shape via get_output_shape(node_idx). The frontend's translate_weight does
100-
// the dequant / repacking / requantization, so the decoder never builds OV nodes itself.
101-
102-
// Model-level RoPE configuration, used by TranslateSession::preprocess to pre-build
103-
// the shared rope sin/cos. has_rope() == false means the model uses no RoPE.
104-
virtual bool has_rope() const = 0;
105-
106-
// When true, each ROPE op generates its own sin/cos from its per-op rope_config
107-
// (e.g. gemma4 where SWA and global layers have different n_dims). The shared
108-
// rope_cos/rope_sin tensor is NOT added to the tensor map.
109-
virtual bool use_per_op_rope() const = 0;
110-
111-
virtual RopeConfig get_rope_config() const = 0;
112-
113-
virtual std::map<std::string, std::string> get_kv_param_res_names() const = 0;
114-
115-
virtual bool is_static() const = 0;
116-
117-
virtual bool is_stateful() const = 0;
118-
119-
virtual bool is_swa_layer(int layer) const = 0;
98+
// visit_subgraph with the genuine ggml leaf op type "GGML_OP_NONE": the decoder marks it as a
99+
// weight by exposing the raw weight bytes via get_attribute<ov::Tensor>("data"), the ggml
100+
// quant type name via get_attribute<std::string>("quant_type") (e.g. "Q4_K", "F16") and the
101+
// logical [rows, cols] shape via get_output_shape(). The frontend's translate_weight does the
102+
// dequant / repacking / requantization, so the decoder never builds OV nodes itself. (Model
103+
// inputs are also GGML_OP_NONE leaves, but they are returned via get_model_inputs() and
104+
// resolved to Parameters before the walk, so they carry no "data".)
105+
106+
// RoPE configuration, exposed through get_attribute<RopeConfig>("rope_config"):
107+
// - at model scope, used by TranslateSession::preprocess to pre-build the shared rope
108+
// sin/cos table (skipped when RopeConfig::n_dims == 0, i.e. no RoPE, or per_op == true);
109+
// - at node scope, the ROPE translator reads the same key for the op's own config.
120110
};
121111

122112
} // namespace gguf
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (C) 2018-2026 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#pragma once
6+
7+
#include "openvino/frontend/gguf/visibility.hpp"
8+
#include "openvino/op/op.hpp"
9+
10+
namespace ov {
11+
namespace frontend {
12+
namespace gguf {
13+
14+
// Internal placeholder op for a ggml SET_ROWS (a scatter-write of rows into a destination
15+
// tensor). translate_set_rows always emits this op instead of a device-specific ScatterUpdate,
16+
// so conversion is identical regardless of execution mode. Inputs:
17+
// input 0: data (rows to write, reshaped to the destination layout [1, 1, seq, emb])
18+
// input 1: indices (destination row indices, squeezed)
19+
// input 2: dst (the tensor written into -- a Parameter for a KV cache)
20+
// The output is the updated tensor; both the cache Result and the attention read path consume it.
21+
//
22+
// A normalization-stage lowering replaces every SetRows: the built-in LowerSetRows (default)
23+
// rebuilds the llama.cpp-faithful ScatterUpdate form. A caller may register (via
24+
// DecoderTransformationExtension) an alternative lowering that recognizes the SetRows feeding
25+
// attention as a KV-cache write and turns it into an OpenVINO stateful ReadValue/Concat/Assign
26+
// subgraph. The op never reaches a compiled model. It is public so a caller-side lowering pass
27+
// can match it by type.
28+
class GGUF_FRONTEND_API SetRows : public ov::op::Op {
29+
public:
30+
OPENVINO_OP("SetRows", "gguf");
31+
32+
SetRows() = default;
33+
SetRows(const ov::Output<ov::Node>& data,
34+
const ov::Output<ov::Node>& indices,
35+
const ov::Output<ov::Node>& dst);
36+
37+
void validate_and_infer_types() override;
38+
std::shared_ptr<ov::Node> clone_with_new_inputs(const ov::OutputVector& new_args) const override;
39+
};
40+
41+
} // namespace gguf
42+
} // namespace frontend
43+
} // namespace ov

src/frontends/gguf/src/CMakeLists.txt

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22
# SPDX-License-Identifier: Apache-2.0
33
#
44

5-
# The GGUF frontend is an MVP port of llama.cpp's ggml-openvino frontend. It is built
6-
# only when ENABLE_OV_GGUF_FRONTEND is ON and is marked SKIP_INSTALL so it is never
7-
# shipped in the OpenVINO release package (it is for development / testing only).
5+
# The GGUF frontend is an MVP port of llama.cpp's ggml-openvino frontend, built only when
6+
# ENABLE_OV_GGUF_FRONTEND is ON. It installs the library + headers alongside the other
7+
# frontends (LINKABLE_FRONTEND, no SKIP_INSTALL) so callers -- the llama.cpp ggml-openvino
8+
# backend and OpenVINO GenAI -- can link openvino::frontend::gguf directly.
9+
#
10+
# It is intentionally NOT discoverable via FrontEndManager: it exports no plugin-registration
11+
# entry points (get_api_version / GetFrontEndData), so it never appears in available_front_ends()
12+
# and is never auto-selected by load_by_model / load_by_framework, even though it is installed in
13+
# the frontend directory. See the discoverability note in frontend.cpp. This keeps the
14+
# not-yet-production frontend hidden from the generic model-loading API while remaining usable by
15+
# direct linkers.
816
ov_add_frontend(NAME gguf
917
LINKABLE_FRONTEND
10-
SKIP_INSTALL
1118
FILEDESCRIPTION "FrontEnd to convert GGUF models"
12-
LINK_LIBRARIES openvino::util openvino::core::dev)
19+
LINK_LIBRARIES openvino::util openvino::core::dev openvino::offline_transformations)
1320

1421
ov_build_target_faster(openvino_gguf_frontend PCH)
1522

@@ -27,8 +34,5 @@ elseif(MSVC)
2734
target_compile_options(openvino_gguf_frontend PRIVATE /wd4244 /wd4267)
2835
endif()
2936

30-
# SKIP_INSTALL keeps the frontend out of every install component, so it never ships
31-
# in the release package and is never placed in the OpenVINO frontend search
32-
# directory (where FrontEndManager would try to load it). The unit tests do not link
33-
# this library: they compile the frontend sources directly (see tests/CMakeLists.txt),
34-
# keeping the test binary self-contained.
37+
# The unit tests do not link this library: they compile the frontend sources directly
38+
# (see tests/CMakeLists.txt), keeping the test binary self-contained.

0 commit comments

Comments
 (0)