include llama.cpp since there were some edits

This commit is contained in:
zefie committed 2026-11-01 16:32:35 -05:00
1 parent db7f28d087
commit 8c66990853
3918 files changed
+1640170 -1

No files matched your search

@@ -0,0 +1,149 @@
---
# Override root .clang-format
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AccessModifierOffset: -4
Language: Cpp
AlignAfterOpenBracket: Align
AlignArrayOfStructures: Left
AlignConsecutiveBitFields: AcrossComments
AlignConsecutiveMacros: AcrossComments
# AlignConsecutiveShortCaseStatements: AcrossComments
AlignEscapedNewlines: Left # LeftWithLastLine
AlignOperands: Align
AlignTrailingComments:
Kind: Always
OverEmptyLines: 1
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: false
# AllowBreakBeforeNoexceptSpecifier: OnlyWithParen
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: Inline
AllowShortLoopsOnASingleLine: false
AlwaysBreakBeforeMultilineStrings: true
# Treat CUDA keywords/attributes as "attribute macros" and avoid breaking lines inside them
AttributeMacros:
- __host__
- __device__
- __global__
- __forceinline__
- __launch_bounds__
BinPackArguments: true
BinPackParameters: false # OnePerLine
BitFieldColonSpacing: Both
# BreakAdjacentStringLiterals: true
BreakAfterAttributes: Never
BreakBeforeBinaryOperators: None
BreakBeforeInlineASMColon: OnlyMultiline
BreakBeforeTernaryOperators: false
# BreakBinaryOperations: Never
BreakConstructorInitializers: AfterColon
# BreakFunctionDefinitionParameters: false
BreakInheritanceList: AfterComma
BreakStringLiterals: true
# BreakTemplateDeclarations: Yes
ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
DerivePointerAlignment: false
DisableFormat: false
EmptyLineBeforeAccessModifier: Leave
EmptyLineAfterAccessModifier: Never
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '".*"'
Priority: 1
SortPriority: 0
- Regex: '^<.*\.h>'
Priority: 2
SortPriority: 0
- Regex: '^<.*'
Priority: 3
SortPriority: 0
- Regex: '.*'
Priority: 4
SortPriority: 0
IncludeIsMainRegex: '([-_](test|unittest))?$'
IncludeIsMainSourceRegex: ''
IndentAccessModifiers: false
IndentExternBlock: NoIndent
IndentGotoLabels: false
IndentPPDirectives: AfterHash
IndentWidth: 4
IndentWrappedFunctionNames: false
InsertBraces: true # NOTE: may lead to incorrect formatting
InsertNewlineAtEOF: true
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
LambdaBodyIndentation: Signature
LineEnding: LF
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PPIndentWidth: -1
PackConstructorInitializers: CurrentLine
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
PointerAlignment: Middle
QualifierAlignment: Left
#QualifierOrder: ['static', 'inline', 'friend', 'constexpr', 'const', 'volatile', 'type', 'restrict']
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- 'c++'
- 'C++'
CanonicalDelimiter: ''
ReferenceAlignment: Middle
ReflowComments: false # IndentOnly
SeparateDefinitionBlocks: Always
SortIncludes: CaseInsensitive
SortUsingDeclarations: LexicographicNumeric
SpaceAfterCStyleCast: true
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: Never
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceBeforeSquareBrackets: false
Standard: c++17
TabWidth: 4
UseTab: Never
WhitespaceSensitiveMacros: ['STRINGIZE']
...
@@ -0,0 +1,33 @@
find_package(OpenVINO REQUIRED COMPONENTS Runtime Threading)
find_package(OpenCL REQUIRED)
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")
ggml_add_backend_library(ggml-openvino
${GGML_SOURCES_OPENVINO}
${GGML_HEADERS_OPENVINO}
)
target_link_libraries(ggml-openvino PRIVATE openvino::runtime openvino::threading 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")
target_compile_options(ggml-openvino PRIVATE -Wno-gnu-zero-variadic-macro-arguments)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(ggml-openvino PRIVATE -Wno-pedantic)
endif()
if (WIN32)
target_link_libraries(ggml-openvino PRIVATE psapi)
endif()
if (GGML_OPENVINO)
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64")
elseif (CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "amd64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64")
else()
message(FATAL_ERROR "OpenVINO: OpenVINO toolkit supports x86-64 and arm64 but not ${CMAKE_SYSTEM_PROCESSOR}")
endif()
endif()
@@ -0,0 +1,44 @@
# Compiled model cache
`GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR` exports compiled CPU/GPU graphs with their weights. It bypasses the plugin-level `GGML_OPENVINO_CACHE_DIR` and uses `OPTIMIZE_SPEED`, so weightless caching is disabled.
One directory can hold blobs for different models and compilation settings. Run each intended workload once to export its dynamic graph:
```sh
GGML_OPENVINO_DEVICE=GPU \
GGML_OPENVINO_NATIVE_SOFTPLUS=1 \
GGML_OPENVINO_DISABLE_KV_SLICE=1 \
GGML_OPENVINO_REQUANT_KQUANT=q4_asym64_all \
GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR=/path/to/qwen-cache \
./build/ReleaseOV/bin/llama-bench -m /path/to/model.gguf -r 1
```
`GGML_OPENVINO_SPILL_DIR` remains optional for this first run. Wait for the `model cache WROTE` message and completion of the workload before stopping it. Compatible prefill and decode graphs share one blob and manifest. A graph with different ports or incompatible shapes gets an exact entry instead; interrupted exports are not cache hits.
On later runs, supply the same compilation settings and enable `GGML_OPENVINO_COMPILED_MODEL_CACHE_ONLY=1`:
```sh
GGML_OPENVINO_DEVICE=GPU \
GGML_OPENVINO_NATIVE_SOFTPLUS=1 \
GGML_OPENVINO_DISABLE_KV_SLICE=1 \
GGML_OPENVINO_REQUANT_KQUANT=q4_asym64_all \
GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR=/path/to/qwen-cache \
GGML_OPENVINO_COMPILED_MODEL_CACHE_ONLY=1 \
./build/ReleaseOV/bin/llama-bench -m /path/to/model.gguf -r 1
```
Cache-only mode allocates backend address space without filling weight pages. On Windows, this also uses system commit capacity. The model-buffer size in the loader log is this virtual size. Weight uploads only record source identity; they do not read or requantize the weights. Graph conversion and compilation are skipped. Runtime buffers are still allocated and populated normally.
Cache-only mode uses the settings provided by the current process. Keep these values exactly the same, including set versus unset: `GGML_OPENVINO_REQUANT_KQUANT`, `GGML_OPENVINO_NATIVE_SOFTPLUS`, `GGML_OPENVINO_DISABLE_KV_SLICE`, `GGML_OPENVINO_MANUAL_GQA_ATTN`, `GGML_OPENVINO_STATEFUL_EXECUTION`, `GGML_OPENVINO_DISABLE_KV_STATE_RELAYOUT`, `GGML_OPENVINO_DISABLE_REMOTE_OUTPUTS`, `GGML_OPENVINO_REDUCE_COMPILE_MEM`, `GGML_OPENVINO_MEMORY_OPTIMIZE`, `GGML_OPENVINO_PROFILING`, and `GGML_OPENVINO_DEBUG_NODE`. On GPU, also repeat `GGML_OPENVINO_MOE_OP=0` if used. `GGML_OPENVINO_SPILL_DIR` is optional on the first run and ignored in cache-only mode; host-weight release is disabled in cache-only mode.
A missing or incompatible graph fails with an error instead of compiling with absent weights. The fingerprint uses the dynamic graph's topology, ports, model parameters, weights, settings, and OpenVINO version; changing only dynamic token or KV sizes does not require a new entry. A different workload can still require another graph; populate it first without cache-only mode.
## Restrictions
- Cache-only mode requires Linux or Windows and mmap loading (`--load-mode mmap`, or the default when all selected devices support mmap). Do not use tensor validation or mlock when trying to avoid weight reads.
- On Windows, the backend commits virtual memory for its buffers without touching weight pages. Large models can still reach the system commit limit.
- The model must execute entirely on OpenVINO, with dynamic CPU/GPU graphs and in-process caching enabled. Static/NPU execution and CPU fallback are unsupported in cache-only mode.
- GGUF metadata, tokenizer data, tensor descriptors, and graph construction are still needed. The llama.cpp loader is unchanged: depending on its prefetch settings, it may request pages with `MAP_POPULATE` or read-ahead on Linux, or `PrefetchVirtualMemory` on Windows. Non-mmap loading also reads the payload before the backend sees it.
- File identity, size, modification/change timestamps, tensor offsets, graph structure, settings, and OpenVINO version identify cache entries. Linux uses device/inode and Windows uses volume serial/file index. Replacing, copying, or modifying a GGUF invalidates its entries. This avoids reading weight bytes and ties the cache to the local source files. Keep those files unchanged throughout loading and inference.
- Use the same target device and compatible OpenVINO/plugin installation. Import support depends on the plugin; the tested CPU plugin cannot import MoE graphs containing `GatherMatmulCompressed`. GPU MoE and CPU dense graph imports were tested.
- Blobs contain weights and can approach model size for each compiled graph. Import still reads those blobs and initializes the device.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,507 @@
#pragma once
#include "ggml-backend-impl.h"
#include "ggml-backend.h"
#include "ggml.h"
#include "openvino/decoder.h"
#include <cstdint>
#include <cstring>
#include <map>
#include <memory>
#include <openvino/core/partial_shape.hpp>
#include <optional>
#include <set>
#include <string>
#include <vector>
struct ModelParams {
int ctx = -1;
int ctx_per_seq = -1;
int ctx_per_seq_swa = -1;
int n_seq = 1;
int n_heads_kv = -1;
// Per-layer KV head count. gemma-4 12B interleaves 8 x 256 sliding layers with 1 x 512
// full-attention layers, so no single scalar describes every layer. Keyed by layer, not by
// layer TYPE, because the SWA classification depends on the context size (extents tie at a
// small -c) while the head count does not.
std::map<int, int> n_heads_kv_per_layer;
int head_size = -1;
int state_size = -1; // for SSM molels, eg qwen35
int32_t rope_params[16] = {};
int n_rs_slots = -1;
bool has_rs_rollback = false;
bool mixed_rope_params = false;
bool is_cacheless_attn = false;
std::vector<int> swa_layers;
// The sliding-window mask tensor, identified in compute_llm_params() by grouping attention
// layers on the mask they consume. Only used to tell the two masks apart when naming OV
// parameters -- both carry the same tensor name. Null when the graph has a single mask.
const ggml_tensor * swa_mask = nullptr;
std::vector<std::string> kv_names;
size_t kv_buffer_ctx_id = 0;
bool same_rope_params(const ModelParams & other) const {
return mixed_rope_params == other.mixed_rope_params &&
memcmp(rope_params, other.rope_params, sizeof(int32_t) * 16) == 0;
}
bool can_reuse_dynamically(const ModelParams & other) const {
return same_rope_params(other) && n_rs_slots == other.n_rs_slots &&
has_rs_rollback == other.has_rs_rollback;
}
bool can_reuse_statically(const ModelParams & other) const {
return same_rope_params(other) && ctx == other.ctx && n_rs_slots == other.n_rs_slots &&
has_rs_rollback == other.has_rs_rollback;
}
bool kv_buffer_changed(const ModelParams & other) const { return kv_buffer_ctx_id != other.kv_buffer_ctx_id; }
};
struct ComputeParams {
int n_seq_active = 1;
int seq_active_start = 0;
int attention_size = -1;
int attention_size_swa = -1;
int attention_size_static = -1; // encoder/cross-attn KV fill level (whisper)
// Sliding window width, read back from the band of ggml's own SWA mask. ggml never passes
// n_swa down to a backend, but fill_mask() bakes it into the mask contents, so the widest
// unmasked row recovers it. Shorter than n_swa while the sequence is still short, which is
// harmless: every causal pair is inside the window then anyway.
int swa_window = -1;
int input_len = -1;
int token_len_per_seq = -1;
int past_kv_len = -1;
int output_len = 1;
int cache_rs_reset_idx = -1;
int cache_rs_reset_len = -1;
// SSM/DeltaNet models otionally clear cache_r and cache_s of certain slots in the cgraph
// 3: [ 18432, 4, 1, 1] RESHAPE cache_r_l0 (reshaped)
// [ 18432, 4, 1, 1] 0: NONE cache_r_l0
// 4: [ 18432, 1, 1, 1] VIEW cache_r_l0 (reshaped) (view)
// [ 18432, 4, 1, 1] 0: RESHAPE cache_r_l0 (reshaped)
// 5: [ 18432, 1, 1, 1] SCALE cache_r_l0 (reshaped) (view) (view)
// [ 18432, 1, 1, 1] 0: VIEW cache_r_l0 (reshaped) (view)
int s_copy_active_slot_len = -1;
// SSM/DeltaNet models otionally reorder slots of state cache, to make the active slots contiguous
// leaf_5 is the inp->s_copy in llama-graph.cpp, eg if there are 8 slots in total and slot 3 and 7
// are active in the current batch, leaf_5 will be [3, 7, 5, 6, 4]
// 6: [ 2, 1, 1, 1] VIEW (view)
// [ 2, 1, 1, 1] 0: NONE leaf_5
// 7: [ 18432, 2, 1, 1] GET_ROWS conv_states-0
// [ 18432, 4, 1, 1] 0: RESHAPE cache_r_l0 (reshaped)
// [ 2, 1, 1, 1] 1: VIEW (view)
// 8: [ 0, 1, 1, 1] VIEW (view)
// [ 2, 1, 1, 1] 0: NONE leaf_5
// 9: [ 18432, 0, 1, 1] GET_ROWS node_9
// [ 18432, 4, 1, 1] 0: RESHAPE cache_r_l0 (reshaped)
// [ 0, 1, 1, 1] 1: VIEW (view)
// 10: [ 18432, 0, 1, 1] VIEW cache_r_l0 (view)
// [ 18432, 4, 1, 1] 0: NONE cache_r_l0
// 11: [ 18432, 0, 1, 1] CPY cache_r_l0 (view) (copy of )
// [ 18432, 0, 1, 1] 0: GET_ROWS node_9
// [ 18432, 0, 1, 1] 1: VIEW cache_r_l0 (view)
struct RsWriteback {
int slot_begin = 0; // first cache slot written by the CPY
int src_begin = -1; // first source column copied by a conv-state CPY
};
std::map<std::string, RsWriteback> rs_writebacks;
// Destination slot offset of each state cache writeback CPY node, keyed by node name. It
// changes with the batch (kv head, active sequence count) and, with rollback enabled
// (cparams.n_rs_seq > 0), the conv state is written back once per snapshot slot. Passed to the
// cached model as a runtime input. Dynamic models also receive the source-side offset; static
// models use a fixed end-anchored offset in the translator.
};
// defined below; declared here because GgmlOvDecoder uses it inline
std::optional<int> extract_layer_from_name(const std::string & name);
// detects the MoE expert-plane-sum ADD chain (see definition); used by supports_op too
bool is_moe_expert_sum_add(const ggml_tensor * node);
class GgmlOvDecoder : public ov::frontend::ggml::GgmlDecoder {
public:
static std::string get_tensor_name(const ggml_cgraph * cgraph, const ggml_tensor * tensor);
struct NodeInfo {
ggml_tensor * node;
std::string node_name;
std::string node_op_type;
std::map<std::string, ggml_tensor *> node_inputs;
std::map<std::string, std::vector<std::pair<std::string, ggml_tensor *>>> node_inputs_views;
std::vector<std::string> node_inputs_names;
int node_op_case = 0;
void * data_addr;
};
// Graph decoder
GgmlOvDecoder(ggml_cgraph * cgraph,
ModelParams & model_params,
ComputeParams & compute_params,
std::map<std::string, std::shared_ptr<ov::Node>> & model_weights,
bool is_static,
bool is_stateful = false,
bool model_is_splitted = false,
bool is_prefill = false,
int prefill_chunk_size = 256);
// Naive graph decoder
GgmlOvDecoder(ggml_cgraph * cgraph, std::map<std::string, std::shared_ptr<ov::Node>> & model_weights);
virtual ov::Any get_attribute(const std::string & name) const override {
return nullptr;
GGML_UNUSED(name);
}
virtual ov::PartialShape get_input_shape(int node_idx, const std::string & name) const override;
virtual std::vector<size_t> get_input_stride(int node_idx, const std::string & name) const override;
virtual size_t get_view_input_size(int node_idx, const std::string & name) const override;
virtual size_t get_view_input_offset(int node_idx, const std::string & name, size_t view_index) const override;
virtual size_t get_view_input_src_offset(int node_idx, const std::string & name, size_t view_index) const override;
virtual std::vector<size_t> get_view_input_stride(int node_idx,
const std::string & name,
size_t view_index) const override;
virtual std::vector<size_t> get_view_input_src_stride(int node_idx,
const std::string & name,
size_t view_index) const override;
virtual ov::Shape get_view_input_ggml_shape(int node_idx,
const std::string & name,
size_t view_index) const override;
virtual ov::Shape get_view_input_src_ggml_shape(int node_idx,
const std::string & name,
size_t view_index) const override;
virtual ov::PartialShape get_view_input_ov_shape(int node_idx,
const std::string & name,
size_t view_index) const override;
virtual ov::PartialShape get_view_input_src_ov_shape(int node_idx,
const std::string & name,
size_t view_index) const override;
virtual std::string get_view_input_name(int node_idx, const std::string & name, size_t view_index) const override;
virtual std::string get_view_input_src_name(int node_idx,
const std::string & name,
size_t view_index) const override;
virtual ov::element::Type get_input_type(int node_idx, const std::string & name) const override;
virtual size_t get_input_size() const override;
virtual size_t get_input_size(int node_idx) const override;
virtual void get_input_node(size_t input_port_idx,
std::string & producer_name,
std::string & producer_output_port_name,
size_t & producer_output_port_index) const override {
GGML_UNUSED(input_port_idx);
GGML_UNUSED(producer_name);
GGML_UNUSED(producer_output_port_name);
GGML_UNUSED(producer_output_port_index);
}
virtual std::vector<std::string> get_input_names(int node_idx) const override;
virtual ov::PartialShape get_output_shape(int node_idx) const override;
virtual ov::element::Type get_output_type(int node_idx) const override;
virtual std::vector<size_t> get_output_stride(int node_idx) const override;
virtual int32_t * get_input_op_params(int node_idx, const std::string & name) const override;
virtual int32_t * get_output_op_params(int node_idx) const override;
virtual size_t get_output_op_offset(int node_idx) const override;
virtual std::vector<std::string> get_output_names(int node_idx) const override;
virtual std::string get_inplace_op_src(int node_idx) const override;
virtual bool is_view_like_alias_of(int node_idx, const std::string & view_src_name) const override;
virtual const std::string & get_op_type() const override;
virtual const std::string & get_op_type(int node_idx) const override;
virtual const std::string & get_op_name() const override;
virtual const std::string & get_op_name(int node_idx) const override;
virtual int32_t get_op_dynamic_dim(int node_idx) const override;
virtual void visit_subgraph(
std::function<void(std::shared_ptr<GgmlDecoder>, int node_idx)> node_visitor) const override;
ggml_tensor * get_input_ggml_tensor(const std::string & name) const { return m_inputs.at(name); }
virtual int get_op_case(int node_idx) const override { return m_node_info_list[node_idx].node_op_case; }
virtual const std::map<std::string, ov::frontend::ggml::ModelInputInfo> & get_model_inputs() const override {
return m_model_inputs;
}
virtual const std::map<std::string, ov::frontend::ggml::ModelExtraInputInfo> & get_model_extra_inputs() const override {
return m_model_extra_inputs;
}
virtual const std::map<std::string, std::shared_ptr<ov::Node>> & get_model_weights() const override {
return m_model_weights;
}
virtual std::set<std::string> get_model_output_names() const override { return m_model_output_names; }
const std::map<std::string, ggml_tensor *> & get_model_outputs() const { return m_model_outputs; }
virtual int get_ctx_size() const { return m_model_params.ctx; }
virtual int get_ctx_per_seq() const { return m_model_params.ctx_per_seq; }
virtual int get_ctx_per_seq_swa() const { return m_model_params.ctx_per_seq_swa; }
virtual int get_n_seq() const { return m_model_params.n_seq; }
virtual int is_swa_layer(int layer) const override {
return std::find(m_model_params.swa_layers.begin(), m_model_params.swa_layers.end(), layer) !=
m_model_params.swa_layers.end();
}
// KV head count for one layer. Sliding and full layers can differ (gemma-4 12B), so callers
// that reinterpret a KV buffer must use this and not the model-level n_heads_kv.
int get_n_heads_kv_for_layer(int layer) const {
auto it = m_model_params.n_heads_kv_per_layer.find(layer);
return it != m_model_params.n_heads_kv_per_layer.end() ? it->second : m_model_params.n_heads_kv;
}
// Same, for a KV cache tensor: its layer comes from the leaf name (cache_k_l<N>).
int get_n_heads_kv_for_tensor(const ggml_tensor * kv_tensor) const {
if (auto layer = extract_layer_from_name(std::string(kv_tensor->name)); layer.has_value()) {
return get_n_heads_kv_for_layer(layer.value());
}
return m_model_params.n_heads_kv;
}
int get_past_kv_len() const { return m_compute_params.past_kv_len; }
int get_input_len() const { return m_compute_params.input_len; }
virtual int32_t * get_rope_params() const override { return const_cast<int32_t *>(m_model_params.rope_params); }
virtual bool has_mixed_rope_params() const override { return m_model_params.mixed_rope_params; }
virtual int get_ssm_state_size() const override { return m_model_params.state_size; }
virtual std::map<std::string, std::string> get_kv_param_res_names() const override;
virtual bool is_static() const override { return m_is_static; }
virtual bool is_stateful() const override { return m_is_stateful; }
int get_static_n_tokens() const { return m_is_prefill ? m_prefill_chunk_size : 1; }
virtual bool is_splited_model() const override { return m_model_is_splitted; }
ov::PartialShape get_graph_input_shape(const ggml_tensor * op,
const ggml_tensor * input,
int dynamic_dim_index = -1) const;
static void dump_cgraph(const ggml_cgraph * cgraph, std::string & filename);
static std::shared_ptr<ov::Node> create_weight_node(ggml_tensor * tensor, bool naive = false);
static std::map<std::string, std::shared_ptr<ov::Node>> create_weight_nodes(ggml_cgraph * cgraph,
bool naive = false);
// Collect just the set of weight-tensor names referenced by the graph, without
// building (or requantizing) any OV weight nodes. Used by topology checks like
// is_model_splitted that only need name membership.
static std::set<std::string> collect_weight_names(ggml_cgraph * cgraph);
const ggml_tensor * get_tensor_used_op(const ggml_tensor * tensor) const;
const ggml_tensor * get_tensor_from_name(const std::string & name) const;
void clear_model_weights() { m_model_weights.clear(); }
static std::pair<ModelParams, ComputeParams> compute_llm_params(ggml_cgraph * cgraph, bool is_static);
ModelParams get_model_params() const { return m_model_params; }
ComputeParams get_compute_params() const { return m_compute_params; }
void set_model_params(const ModelParams & model_params) { m_model_params = model_params; }
void set_compute_params(const ComputeParams & compute_params) { m_compute_params = compute_params; }
bool m_is_static = false;
bool m_is_stateful = false;
bool m_is_prefill = false;
bool m_naive = false;
int m_prefill_chunk_size = 0;
bool m_model_is_splitted = false; // label the cgraph is splited or not
static ov::Shape get_shape(const ggml_tensor * tensor);
static std::vector<size_t> get_stride(const ggml_tensor * tensor);
static ov::element::Type get_ov_type(const ggml_tensor * tensor);
static std::string compute_op_type(const ggml_tensor * node);
void add_extra_inputs();
void update_io(ggml_cgraph * cgraph);
bool is_bound_to(const ggml_cgraph * cgraph) const;
static bool is_inp_tok(const ggml_tensor * tensor, const ggml_tensor * op) {
return op->op == GGML_OP_GET_ROWS && tensor == op->src[1] && op->src[0]->op == GGML_OP_NONE;
}
static bool is_inp_pos(const ggml_tensor * tensor, const ggml_tensor * op) {
return op->op == GGML_OP_ROPE && tensor == op->src[1];
}
// IMROPE and VISION pack 4 stacked position planes (t/h/w/e) into inp_pos, each of length
// n_tokens; other modes carry a single position per token.
static int get_inp_pos_n_planes(const ggml_tensor * op) {
const int mode = op->op_params[2];
return (mode == GGML_ROPE_TYPE_IMROPE || mode == GGML_ROPE_TYPE_VISION || (mode & GGML_ROPE_TYPE_MROPE)) ? 4 : 1;
}
static bool is_inp_emb(const ggml_tensor * tensor, const ggml_tensor * op) {
return tensor->op == GGML_OP_GET_ROWS && op->op == GGML_OP_RMS_NORM;
}
static bool is_inp_mask(const ggml_tensor * tensor, const ggml_tensor * op) {
return op->op == GGML_OP_CPY || (op->op == GGML_OP_FLASH_ATTN_EXT && tensor == op->src[3]) ||
(op->op == GGML_OP_SOFT_MAX && tensor == op->src[1]);
}
static bool is_inp_mean(const ggml_tensor * tensor, const ggml_tensor * op) {
return op->op == GGML_OP_MUL_MAT && tensor == op->src[1] && tensor->op == GGML_OP_NONE &&
(tensor->flags & GGML_TENSOR_FLAG_INPUT) && tensor->type == GGML_TYPE_F32 &&
op->src[0] != nullptr && op->src[0]->op != GGML_OP_NONE;
}
// per-token embedding scale [1, n_tokens] (inp->scale_rows in llama-graph.cpp)
static bool is_inp_scale_rows(const ggml_tensor * tensor, const ggml_tensor * op) {
return op->op == GGML_OP_MUL && tensor == op->src[1] && strcmp(tensor->name, "inp_scale_rows") == 0;
}
static bool is_rope_freqs_weight(const ggml_tensor * tensor, const ggml_tensor * op) {
return op->op == GGML_OP_ROPE && tensor == op->src[2];
}
inline static bool is_recurrent_cache(const ggml_tensor * tensor) {
return tensor != nullptr && (strncmp(tensor->name, "cache_r_l", strlen("cache_r_l")) == 0 ||
strncmp(tensor->name, "cache_s_l", strlen("cache_s_l")) == 0 ||
strncmp(tensor->name, "cache_ple_r_l", strlen("cache_ple_r_l")) == 0);
}
inline static bool is_cache(const ggml_tensor * tensor, const ggml_tensor * op) {
return is_recurrent_cache(tensor) || is_kvcache(tensor, op);
}
inline static bool is_kvcache(const ggml_tensor * tensor, const ggml_tensor * op) {
if (tensor == nullptr || is_recurrent_cache(tensor)) {
return false;
}
return (tensor->buffer != nullptr && tensor->buffer->usage == GGML_BACKEND_BUFFER_USAGE_ANY) ||
(op != nullptr && op->op == GGML_OP_SET_ROWS && op->src[2] == tensor);
}
inline static bool is_conv_state_writeback(const ggml_tensor * node) {
return node->op == GGML_OP_CPY && node->view_src != nullptr && is_recurrent_cache(node->view_src) &&
node->src[0] != nullptr && node->src[0]->op == GGML_OP_VIEW && node->src[0]->src[0] != nullptr &&
node->src[0]->src[0]->op == GGML_OP_CONCAT && node->src[1] != nullptr &&
node->src[1]->op == GGML_OP_VIEW && node->src[1]->view_src == node->view_src;
}
static bool is_kv_idx(const ggml_tensor * tensor, const ggml_tensor * op) {
return op->op == GGML_OP_SET_ROWS && op->src[1] == tensor;
}
bool is_swa_mask(const ggml_tensor * tensor) const {
return m_model_params.swa_mask != nullptr && tensor == m_model_params.swa_mask;
}
static bool is_output_idx(const ggml_tensor * tensor, const ggml_tensor * op) {
return op->op == GGML_OP_GET_ROWS && tensor == op->src[1] && op->src[0]->op != GGML_OP_NONE &&
op->src[1]->op == GGML_OP_NONE;
}
// the state permutation index input used in SSM/DeltaNet models (inp->s_copy in llama-graph.cpp)
static bool is_inp_s_copy(const ggml_tensor * tensor, const ggml_tensor * op) {
return op->op == GGML_OP_GET_ROWS && tensor == op->src[1] &&
op->src[0]->buffer->usage == GGML_BACKEND_BUFFER_USAGE_ANY;
}
std::string get_graph_input_ov_name(const ggml_tensor * tensor, const ggml_tensor * op) const {
if (is_inp_pos(tensor, op)) {
return "inp_pos";
}
if (is_inp_emb(tensor, op)) {
return "embd";
}
if (is_inp_mask(tensor, op)) {
// Give the two attention masks distinct OV parameter names.
//
// An interleaved-SWA model builds one full-attention mask and one sliding-window mask,
// but build_attn_inp_kq_mask() names them identically, so keying a parameter off
// tensor->name alone makes the second mask OVERWRITE the first in m_model_inputs: both
// attention types then read a single parameter, and the windowed layers silently run
// against an unbanded mask. Disambiguate using the SWA layer set computed in
// compute_llm_params(), which classifies by mask tensor identity rather than by name.
//
// When no SWA layer was found there is only one mask in play, so the plain name is
// correct and no _swa parameter is created.
if (m_model_params.swa_layers.empty()) {
return "self_kq_mask";
}
return is_swa_mask(tensor) ? "self_kq_mask_swa" : "self_kq_mask";
}
return tensor->name;
}
private:
void set_input_output();
int compute_op_case(const ggml_tensor * node) const;
bool node_is_used_as_src(const int node_idx);
void compute_model_inputs();
void compute_model_outputs();
// True if tensor is the inp->s_copy index leaf gathered by a recurrent state cache GET_ROWS
// (possibly through a VIEW), so it gets a dynamic [1,1,1,-1] graph-input shape.
bool is_s_copy_leaf(const ggml_tensor * tensor) const;
// Infer and propagate dynamic-dimension indices for all tensors in the GGML graph.
void compute_node_dynamic_dims();
void validate_cgraph() const;
ggml_cgraph * m_cgraph = nullptr;
std::map<std::string, ggml_tensor *> m_inputs;
std::map<std::string, ov::frontend::ggml::ModelInputInfo> m_model_inputs;
std::map<std::string, ov::frontend::ggml::ModelExtraInputInfo> m_model_extra_inputs;
std::map<std::string, std::shared_ptr<ov::Node>> m_model_weights;
std::map<std::string, ggml_tensor *> m_model_outputs;
std::set<std::string> m_model_output_names;
std::vector<NodeInfo> m_node_info_list;
std::map<ggml_tensor *, int> m_node_dynamic_dims;
ModelParams m_model_params;
ComputeParams m_compute_params;
};
void print_tensor_address_map(const ggml_cgraph * cgraph);
@@ -0,0 +1,669 @@
#include "ggml-openvino-extra.h"
#include "ggml-impl.h"
#include "ggml.h"
#include "model-cache.h"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <openvino/runtime/intel_gpu/ocl/ocl.hpp>
#include <openvino/runtime/intel_npu/level_zero/level_zero.hpp>
#include <openvino/runtime/properties.hpp>
#include <mutex>
#include <optional>
ov::Core & ov_singleton_core() {
static ov::Core core;
return core;
}
static bool has_prefix(const std::string & s, const std::string & prefix) {
return s.size() >= prefix.size() && std::equal(prefix.begin(), prefix.end(), s.begin());
}
static bool is_virtual_routing_device(const std::string & device_name) {
return has_prefix(device_name, "AUTO") || has_prefix(device_name, "MULTI") || has_prefix(device_name, "HETERO");
}
static std::vector<std::string> ov_enumerate_devices() {
std::vector<std::string> result;
for (const auto & device : ov_singleton_core().get_available_devices()) {
if (!is_virtual_routing_device(device)) {
result.push_back(device);
}
}
if (result.empty()) {
result.push_back("CPU");
}
std::sort(result.begin(), result.end());
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
}
std::string ggml_openvino_get_device_description(const std::string & device_name) {
std::string description = device_name;
try {
description = ov_singleton_core().get_property(device_name, ov::device::full_name);
} catch (...) {
return device_name;
}
if (has_prefix(device_name, "NPU")) {
try {
const std::string arch = ov_singleton_core().get_property(device_name, "DEVICE_ARCHITECTURE").as<std::string>();
if (!arch.empty()) {
description += " (NPU " + arch + ")";
}
} catch (...) {
}
}
return description;
}
// requested: GGML_OPENVINO_DEVICE, nullptr if unset. available_devices is never empty (see ov_enumerate_devices)
static std::string resolve_openvino_device_name(const std::vector<std::string> & available_devices,
const char * requested) {
auto available = [&](const std::string & name) {
return std::find(available_devices.begin(), available_devices.end(), name) != available_devices.end();
};
if (requested == nullptr) {
return available("CPU") ? "CPU" : available_devices.front();
}
if (!available(requested)) {
// No fallback to CPU (easy to miss) and no GPU -> GPU.0 alias (with iGPU + dGPU, GPU.0 is often the
// wrong one). List the devices here: --list-devices initializes this backend and would abort too.
std::string list;
for (const std::string & name : available_devices) {
list += "\n " + name + ": " + ggml_openvino_get_device_description(name);
}
GGML_ABORT("GGML OpenVINO Backend: GGML_OPENVINO_DEVICE=%s is not available. "
"Set it to one of the available OpenVINO devices:%s",
requested, list.c_str());
}
return requested;
}
// =====================================================
// Device Configuration Implementations
// =====================================================
void ggml_openvino_device_config::init() {
static std::mutex mutex;
std::lock_guard<std::mutex> lock(mutex);
if (initialized) {
return;
}
// Set up front: a failed OpenCL setup below is not retried on every call
initialized = true;
// All recognized GGML_OPENVINO_* env vars. Their values are cached here
// once at backend init time and read back via ggml_openvino_getenv_str()
// (raw string) or ggml_openvino_getenv_int() (integer / boolean toggle).
static constexpr const char * env_var_names[] = {
// String values (use ggml_openvino_getenv_str)
"GGML_OPENVINO_DEVICE",
"GGML_OPENVINO_CACHE_DIR",
"GGML_OPENVINO_SPILL_DIR",
"GGML_OPENVINO_DEBUG_NODE",
"GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR",
"GGML_OPENVINO_COMPILED_MODEL_CACHE_ONLY",
"GGML_OPENVINO_NPU_COMPILE_CONFIG",
// Integer values (use ggml_openvino_getenv_int)
"GGML_OPENVINO_PREFILL_CHUNK_SIZE",
// Boolean toggles (treated as int flags via ggml_openvino_getenv_int)
"GGML_OPENVINO_STATEFUL_EXECUTION",
"GGML_OPENVINO_PROFILING",
"GGML_OPENVINO_DUMP_CGRAPH",
"GGML_OPENVINO_DUMP_IR",
"GGML_OPENVINO_DEBUG_INPUT",
"GGML_OPENVINO_DEBUG_OUTPUT",
// Force the static (NPU-shape) compute path on any device, e.g. GGML_OPENVINO_DEVICE=CPU,
// to test the static-shape translation without NPUW/real NPU hardware in the loop.
"GGML_OPENVINO_FORCE_STATIC",
"GGML_OPENVINO_PRINT_CGRAPH_TENSOR_ADDRESS",
"GGML_OPENVINO_ENABLE_CACHE",
"GGML_OPENVINO_DISABLE_CACHE",
"GGML_OPENVINO_DISABLE_KV_SLICE",
"GGML_OPENVINO_ENABLE_FALLBACK",
"GGML_OPENVINO_MANUAL_GQA_ATTN",
"GGML_OPENVINO_DISABLE_ELTWISE_RANK_ALIGN",
"GGML_OPENVINO_MOE_OP",
"GGML_OPENVINO_MEMORY_OPTIMIZE",
"GGML_OPENVINO_RELEASE_WEIGHTS",
"GGML_OPENVINO_REDUCE_COMPILE_MEM",
"GGML_OPENVINO_LOG_UNSUPPORTED_OPS",
"GGML_OPENVINO_LOG_SWA_LAYERS",
"GGML_OPENVINO_NATIVE_SOFTPLUS",
"GGML_OPENVINO_DISABLE_REMOTE_OUTPUTS",
"GGML_OPENVINO_REQUANT_KQUANT",
"GGML_OPENVINO_DISABLE_KV_STATE_RELAYOUT",
// Build the precise (but O(n_nodes)) graph cache key. Needed by op tests.
"GGML_OPENVINO_FULL_GRAPH_KEY",
};
for (const char * const & env_var : env_var_names) {
auto * env = getenv(env_var);
if (env) {
environment_variables[env_var] = env;
}
}
available_devices = ov_enumerate_devices();
device_name = resolve_openvino_device_name(available_devices, ggml_openvino_getenv_str("GGML_OPENVINO_DEVICE"));
is_npu = has_prefix(device_name, "NPU");
ggml_openvino_model_cache_init();
const char * cache_dir = ggml_openvino_getenv_str("GGML_OPENVINO_CACHE_DIR");
if (has_prefix(device_name, "NPU")) {
compile_config = {
{"NPU_COMPILER_DYNAMIC_QUANTIZATION", "YES" },
{"NPU_USE_NPUW", "YES" },
{"NPUW_DEVICES", "NPU" },
{"NPUW_FOLD", "YES" },
{"NPUW_WEIGHTS_BANK", "shared"},
{"NPUW_FUNCALL_FOR_ALL", "YES" },
{"NPUW_FUNCALL_ASYNC", "YES" },
{"NPUW_DQ", "YES" },
{"NPUW_DQ_FULL", "NO" },
};
if (cache_dir && strlen(cache_dir) > 0) {
compile_config["NPUW_CACHE_DIR"] = cache_dir;
compile_config.insert(ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE));
}
const char * compilation_mode_params =
ggml_openvino_getenv_str("GGML_OPENVINO_NPU_COMPILE_CONFIG");
if (compilation_mode_params && strlen(compilation_mode_params) > 0) {
compile_config["NPU_COMPILATION_MODE_PARAMS"] = compilation_mode_params;
}
} else if (cache_dir && strlen(cache_dir) > 0) {
compile_config.insert(ov::cache_dir(cache_dir));
compile_config.insert(ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE));
}
if (ggml_openvino_getenv_int("GGML_OPENVINO_PROFILING") >= 2) {
compile_config.insert(ov::enable_profiling(true));
}
// Initialize remote context with queue sharing for GPU
if (has_prefix(device_name, "GPU")) {
// Use the OpenCL context OpenVINO created for this device, so GPU.N gets its own device
cl_context cl_ctx;
try {
auto ov_ctx = ov_singleton_core().get_default_context(device_name).as<ov::intel_gpu::ocl::ClContext>();
cl_ctx = ov_ctx.get();
} catch (const std::exception & e) {
// The consumers of the remote context have no host fallback, and OpenVINO
// already reported the device as present.
GGML_ABORT("ggml-openvino: failed to get the OpenCL context for %s: %s", device_name.c_str(), e.what());
}
cl_int err;
cl_device_id cl_device;
err = clGetContextInfo(cl_ctx, CL_CONTEXT_DEVICES, sizeof(cl_device), &cl_device, nullptr);
if (err != CL_SUCCESS) {
GGML_ABORT("ggml-openvino: failed to get the OpenCL device for %s: %d", device_name.c_str(), err);
}
cl_platform_id cl_platform;
err = clGetDeviceInfo(cl_device, CL_DEVICE_PLATFORM, sizeof(cl_platform), &cl_platform, nullptr);
if (err != CL_SUCCESS) {
GGML_ABORT("ggml-openvino: failed to get the OpenCL platform for %s: %d", device_name.c_str(), err);
}
cl_mem_fill_fn =
(clEnqueueMemFillINTEL_fn) clGetExtensionFunctionAddressForPlatform(cl_platform, "clEnqueueMemFillINTEL");
cl_mem_cpy_fn =
(clEnqueueMemcpyINTEL_fn) clGetExtensionFunctionAddressForPlatform(cl_platform, "clEnqueueMemcpyINTEL");
cl_ulong device_max_alloc = 0;
err = clGetDeviceInfo(cl_device, CL_DEVICE_MAX_MEM_ALLOC_SIZE, sizeof(device_max_alloc), &device_max_alloc,
nullptr);
if (err == CL_SUCCESS) {
max_alloc_size = device_max_alloc;
} else {
// not fatal, ggml then allocates one buffer
GGML_LOG_WARN("Failed to get OpenCL max allocation size: %d\n", err);
}
const cl_queue_properties profiling_properties[] = {
CL_QUEUE_PROPERTIES,
CL_QUEUE_PROFILING_ENABLE,
0,
};
const cl_queue_properties * queue_properties =
ggml_openvino_getenv_int("GGML_OPENVINO_PROFILING") >= 2 ? profiling_properties : nullptr;
cl_queue = clCreateCommandQueueWithProperties(cl_ctx, cl_device, queue_properties, &err);
if (err != CL_SUCCESS) {
GGML_ABORT("ggml-openvino: failed to create the OpenCL queue for %s: %d", device_name.c_str(), err);
}
// Create OpenVINO remote context with queue sharing
remote_context = ov::intel_gpu::ocl::ClContext(ov_singleton_core(), cl_queue);
} else if (has_prefix(device_name, "NPU")) {
// remote tensor is not used for NPU yet
// remote_context = ov_singleton_core().get_default_context(device_name);
}
}
ggml_openvino_device_config::~ggml_openvino_device_config() {
if (cl_queue != nullptr) {
clReleaseCommandQueue(cl_queue);
cl_queue = nullptr;
}
}
// Get the global device config singleton
ggml_openvino_device_config & ggml_openvino_get_device_config() {
static ggml_openvino_device_config config;
return config;
}
// Initialize device config (call during backend init)
void ggml_openvino_init_device_config() {
ggml_openvino_get_device_config().init();
}
// Get the device name
const std::string & ggml_openvino_get_device_name() {
return ggml_openvino_get_device_config().device_name;
}
std::vector<std::string> ggml_openvino_get_available_devices() {
auto & config = ggml_openvino_get_device_config();
config.init();
return config.available_devices;
}
// Get the value of a GGML_OPENVINO_* env var as a string. Returns
// default_value when the var is unset or set to an empty string.
const char * ggml_openvino_getenv_str(const char * var, const char * default_value) {
auto & env_map = ggml_openvino_get_device_config().environment_variables;
auto it = env_map.find(var);
return (it == env_map.end() || it->second.empty()) ? default_value : it->second.c_str();
}
// Get the value of a GGML_OPENVINO_* env var as an int (via std::atoi).
// Returns default_value (0) when the var is unset or empty. Used for both
// integer settings (e.g. GGML_OPENVINO_PREFILL_CHUNK_SIZE) and boolean
// toggles: "0" disables, any non-zero integer enables.
int ggml_openvino_getenv_int(const char * var, int default_value) {
const char * v = ggml_openvino_getenv_str(var, nullptr);
return v ? std::atoi(v) : default_value;
}
bool ggml_openvino_reduce_compile_mem_enabled() {
const char * reduce_compile_mem = ggml_openvino_getenv_str("GGML_OPENVINO_REDUCE_COMPILE_MEM");
if (reduce_compile_mem != nullptr) {
return ggml_openvino_getenv_int("GGML_OPENVINO_REDUCE_COMPILE_MEM") != 0;
}
return ggml_openvino_getenv_int("GGML_OPENVINO_MEMORY_OPTIMIZE") != 0;
}
bool ggml_openvino_release_weights_enabled() {
const char * release_weights = ggml_openvino_getenv_str("GGML_OPENVINO_RELEASE_WEIGHTS");
if (release_weights != nullptr) {
return ggml_openvino_is_gpu() && ggml_openvino_getenv_int("GGML_OPENVINO_RELEASE_WEIGHTS") != 0;
}
return ggml_openvino_is_gpu() && ggml_openvino_getenv_int("GGML_OPENVINO_MEMORY_OPTIMIZE") != 0;
}
// Check if running on NPU
bool ggml_openvino_is_npu() {
return ggml_openvino_get_device_config().is_npu;
}
bool ggml_openvino_is_gpu() {
return has_prefix(ggml_openvino_get_device_name(), "GPU");
}
size_t ggml_openvino_max_alloc_size() {
return ggml_openvino_get_device_config().max_alloc_size;
}
// Get the remote context for the current device (returns empty optional for CPU)
std::optional<ov::RemoteContext> ggml_openvino_get_remote_context() {
return ggml_openvino_get_device_config().remote_context;
}
// Get the compile config for the current device
const ov::AnyMap & ggml_openvino_get_compile_config() {
return ggml_openvino_get_device_config().compile_config;
}
// Get the OpenCL command queue for GPU operations
cl_command_queue ggml_openvino_get_cl_queue() {
return ggml_openvino_get_device_config().cl_queue;
}
// Get the clEnqueueMemFillINTEL function pointer
clEnqueueMemFillINTEL_fn ggml_openvino_get_clEnqueueMemFillINTEL() {
return ggml_openvino_get_device_config().cl_mem_fill_fn;
}
// Get the clEnqueueMemcpyINTEL function pointer
clEnqueueMemcpyINTEL_fn ggml_openvino_get_clEnqueueMemcpyINTEL() {
return ggml_openvino_get_device_config().cl_mem_cpy_fn;
}
// Get requantization type for a tensor type (returns nullopt if no requant needed)
std::optional<ExtraQuantType> ggml_openvino_get_requant_type(const ggml_tensor * tensor, bool no_requant) {
if (no_requant) {
return std::nullopt;
}
if (strncmp(tensor->name, "token_embd.weight", 17) == 0) {
return ((ggml_openvino_is_npu() && tensor->type == GGML_TYPE_Q6_K) ? ExtraQuantType::F16 :
ExtraQuantType::Q8_0_C);
}
if (strncmp(tensor->name, "output.weight", 13) == 0) {
return ExtraQuantType::Q8_0_C;
}
if (ggml_openvino_is_npu()) {
return ExtraQuantType::Q4_0_128;
}
// By default Q6_K/Q5_K are requantized to Q8_0_C, which *inflates* 6- and 5-bit weights to 8
// while the rest of the model stays at 4 bits, and Q4_K keeps its native group-32 layout
// (an f16 scale plus an f16 zero point per 32 weights = 0.125 B/weight of metadata).
// Decode of a large model is bandwidth-bound, so both cost throughput.
//
// GGML_OPENVINO_REQUANT_KQUANT selects a 4-bit target instead. Names are
// q4_<sym|asym><group>[_all]: <sym|asym> says whether a per-group zero point is kept, <group>
// is the group size, and the _all suffix sends Q4_K down the same path (without it only
// Q6_K/Q5_K are touched):
// q4_sym128 Q6_K/Q5_K -> Q4_0_128 (u4, group 128, symmetric)
// q4_sym128_all and Q4_K too -- drops Q4_K's per-32 zero point, which costs some accuracy
// q4_asym64 Q6_K/Q5_K -> Q4_1_64 (u4, group 64, asymmetric)
// q4_asym64_all Q6_K/Q5_K and Q4_K -> Q4_1_64 (u4, group 64, asymmetric)
// native no requantization at all (keep Q6_K/Q5_K as they are)
//
// q4_asym64 leaves Q4_K at its native group 32. Use q4_asym64_all to keep the group size uniform.
const char * rq = ggml_openvino_getenv_str("GGML_OPENVINO_REQUANT_KQUANT");
auto is_opt = [rq](const char * name) {
return rq && strcmp(rq, name) == 0;
};
const bool sym128 = is_opt("q4_sym128");
const bool sym128_all = is_opt("q4_sym128_all");
const bool asym64_all = is_opt("q4_asym64_all");
const bool asym64 = is_opt("q4_asym64");
if (tensor->type == GGML_TYPE_Q4_K) {
if (sym128_all) {
return ExtraQuantType::Q4_0_128;
}
if (asym64_all) {
return ExtraQuantType::Q4_1_64;
}
}
// MoE expert weights (3D, ne[2] = n_expert) stored as Q5_1/Q8_0 are the expert-side
// equivalent of Q6_K/Q5_K: kept at 8 bits by default while the rest of the model is at 4
// (gemma-4 26B-A4B keeps its down projection there). Send them to 4 bits under the same
// option, at group 64 rather than 128: the down expert has k=704, which 64 divides
// (704/64 = 11) and 128 does not.
if (tensor->ne[2] > 1 && (tensor->type == GGML_TYPE_Q5_1 || tensor->type == GGML_TYPE_Q8_0)) {
if (sym128 || sym128_all) {
return ExtraQuantType::Q4_0_64;
}
if (asym64 || asym64_all) {
return ExtraQuantType::Q4_1_64;
}
// TODO: temporary workaround for a known OpenVINO GPU-plugin bug -- remove once the
// plugin computes grouped 8-bit GatherMatmulCompressed correctly. This costs accuracy
// (5/8-bit -> 4-bit) on any model it applies to, so it must not outlive the bug.
//
// On GPU these would otherwise stay in their native *grouped 8-bit* layout, which the GPU
// plugin's GatherMatmulCompressed computes incorrectly -- gemma-4 26B-A4B (whose down
// projection is Q5_1) produces garbage, while the same graph is correct on CPU. It is
// specific to grouped 8 bit: the gate/up experts are grouped u4 *with* a zero point and
// are fine, and Qwen3.5 / granite are fine because their Q5_K/Q6_K down projections
// already requantize to per-channel Q8_0_C (grouped=0). Sending these to grouped 4 bit
// avoids the broken layout and restores correct output.
// Opt out with GGML_OPENVINO_REQUANT_KQUANT=native.
if (ggml_openvino_is_gpu() && !is_opt("native")) {
return ExtraQuantType::Q4_0_64;
}
}
switch (tensor->type) {
case GGML_TYPE_Q6_K:
case GGML_TYPE_Q5_K:
if (sym128 || sym128_all) {
return ExtraQuantType::Q4_0_128;
}
if (asym64 || asym64_all) {
return ExtraQuantType::Q4_1_64;
}
if (is_opt("native")) {
return std::nullopt;
}
return ExtraQuantType::Q8_0_C;
default:
return std::nullopt;
}
}
// =====================================================
// Extracted Layout Calculation
// =====================================================
ggml_openvino_extracted_layout ggml_openvino_get_extracted_layout(const ggml_tensor * tensor, bool use_bias) {
ggml_openvino_extracted_layout layout = {};
layout.is_symmetric = false;
if (!ggml_is_quantized(tensor->type)) {
return layout;
}
// Most quantized weights use the existing 2D extraction path. 3D expert weights for
// MUL_MAT_ID (MoE) are also supported, either as MXFP4 (packed, dedicated branch below) or via the
// generic sizing math below, which is shape-agnostic (based on total element count). Only reject 4D.
if (tensor->ne[3] != 1) {
return layout;
}
// 3D MoE expert weights that are not requantized (see below) always use the exact f16
// zero-point extraction (see extract_quantized_weights), which needs a wider zp slot than
// the packed integer zero point -- must be kept in sync with that function so the buffer
// sizing here matches what process_weight_tensor actually writes.
const bool for_gather_matmul = tensor->ne[2] > 1;
int64_t n_elements = ggml_nelements(tensor);
const size_t alignment = 64; // Good for SIMD
if (tensor->type == GGML_TYPE_MXFP4 && (tensor->ne[2] > 1 || tensor->ne[3] > 1)) {
layout.weights_per_block = 32;
layout.is_symmetric = true;
layout.weights_size = ggml_nbytes(tensor);
layout.weights_offset = 0;
layout.total_size = layout.weights_size;
return layout;
}
// Check if requantization is needed (NPU-specific)
auto requant_type = ggml_openvino_get_requant_type(tensor, use_bias);
if (requant_type.has_value()) {
layout.is_requant = true;
layout.requant_type = requant_type;
// Special case: requant to F16 - just store F16 weights, no scales/zp
if (requant_type.value() == ExtraQuantType::F16) {
layout.weights_size = n_elements * sizeof(uint16_t); // F16 = 2 bytes
layout.total_size = layout.weights_size;
layout.weights_offset = 0;
// No scales/zp for F16
return layout;
}
// Requant to different quantized format (e.g., Q4_0_128)
switch (requant_type.value()) {
case ExtraQuantType::Q4_0_128:
layout.is_u4 = true;
layout.weights_per_block = 128;
layout.is_symmetric = true;
break;
case ExtraQuantType::Q4_1_64:
layout.is_u4 = true;
layout.weights_per_block = 64;
layout.is_symmetric = false;
break;
case ExtraQuantType::Q4_0_64:
layout.is_u4 = true;
layout.weights_per_block = 64;
layout.is_symmetric = true;
break;
case ExtraQuantType::Q4_0_C:
layout.is_u4 = true;
layout.weights_per_block = tensor->ne[0];
layout.is_symmetric = true;
break;
case ExtraQuantType::Q8_0_32:
layout.is_u4 = false;
layout.weights_per_block = 32;
layout.is_symmetric = true;
break;
case ExtraQuantType::Q8_0_C:
layout.is_u4 = false;
layout.weights_per_block = tensor->ne[0];
layout.is_symmetric = true;
break;
case ExtraQuantType::Q8_1_C:
layout.is_u4 = false;
layout.weights_per_block = tensor->ne[0];
break;
default:
GGML_ABORT("Code of re-quantizing to channel-wise is not updated");
}
if (layout.is_requant) {
// Calculate sizes for requantized format
layout.weights_size = layout.is_u4 ? (n_elements / 2) : n_elements;
int64_t n_blocks = n_elements / layout.weights_per_block;
layout.scales_size = n_blocks * sizeof(uint16_t);
// For symmetric quantization, no zp needed (weights stored as signed)
if (layout.is_symmetric) {
layout.zp_size = 0;
} else {
layout.zp_size = layout.is_u4 ? ((n_blocks + 1) / 2) : n_blocks;
}
layout.weights_offset = 0;
layout.scales_offset = ((layout.weights_size + alignment - 1) / alignment) * alignment;
layout.zp_offset = layout.scales_offset + ((layout.scales_size + alignment - 1) / alignment) * alignment;
layout.total_size = layout.zp_offset + layout.zp_size;
layout.total_size = std::max(layout.total_size, ggml_nbytes(tensor));
return layout;
}
}
// Normal extraction (no requant) - determine format based on tensor type
layout.is_u4 = false;
layout.weights_per_block = 32;
layout.is_symmetric = false;
switch (tensor->type) {
case GGML_TYPE_MXFP4:
case GGML_TYPE_Q4_0:
layout.is_u4 = true;
layout.is_symmetric = true;
break;
case GGML_TYPE_Q4_1:
case GGML_TYPE_Q4_K:
layout.is_u4 = true;
break;
case GGML_TYPE_Q8_0:
layout.is_symmetric = true;
break;
case GGML_TYPE_Q5_1:
// u8 weights (5-bit values), asymmetric (scale + zero point)
break;
case GGML_TYPE_Q6_K:
layout.weights_per_block = 16;
layout.is_symmetric = true;
break;
case GGML_TYPE_Q5_K:
break;
default:
// Unsupported quantization type
return layout;
}
// Calculate sizes
// Weights: U4 = n_elements/2 bytes, U8 = n_elements bytes
layout.weights_size = layout.is_u4 ? (n_elements / 2) : n_elements;
// Scales: F16 per block, except MXFP4 which stores one E8M0 byte per block.
int64_t n_blocks = n_elements / layout.weights_per_block;
layout.scales_size = n_blocks * (tensor->type == GGML_TYPE_MXFP4 ? sizeof(uint8_t) : sizeof(uint16_t));
// For symmetric quantization, no zp needed (weights stored as signed). Asymmetric
// for_gather_matmul (3D MoE expert) weights use an exact f16 zero point (see
// extract_quantized_weights/make_int8_weights/make_int4_weights), which needs one f16 per
// block instead of a packed u4/u8 integer zero point.
if (layout.is_symmetric) {
layout.zp_size = 0;
} else if (use_bias || for_gather_matmul) {
layout.zp_size = n_blocks * sizeof(uint16_t);
} else {
layout.zp_size = layout.is_u4 ? ((n_blocks + 1) / 2) : n_blocks;
}
// Layout in buffer: [weights | scales | zp] with alignment
layout.weights_offset = 0;
layout.scales_offset = ((layout.weights_size + alignment - 1) / alignment) * alignment;
layout.zp_offset = layout.scales_offset + ((layout.scales_size + alignment - 1) / alignment) * alignment;
layout.total_size = layout.zp_offset + layout.zp_size;
layout.total_size = std::max(layout.total_size, ggml_nbytes(tensor));
return layout;
}
ggml_openvino_tensor_extra * ggml_openvino_create_tensor_extra(const ggml_tensor * tensor, bool is_remote) {
ov::Shape shape;
for (int i = GGML_MAX_DIMS - 1; i >= 0; --i) {
shape.push_back(static_cast<size_t>(tensor->ne[i]));
}
ov::element::Type element_type;
switch (tensor->type) {
case GGML_TYPE_F32:
element_type = ov::element::f32;
break;
case GGML_TYPE_F16:
element_type = ov::element::f16;
break;
case GGML_TYPE_BF16:
element_type = ov::element::bf16;
break;
case GGML_TYPE_I32:
element_type = ov::element::i32;
break;
case GGML_TYPE_I64:
element_type = ov::element::i64;
break;
default:
// GGML_LOG_WARN("%s: unsupported tensor type for ov::Tensor: %s\n", __func__, ggml_type_name(tensor->type));
return nullptr;
}
auto remote_context = ggml_openvino_get_remote_context();
std::shared_ptr<ov::Tensor> ov_tensor;
if (is_remote) {
GGML_ASSERT(ggml_openvino_is_gpu());
auto gpu_context = remote_context->as<ov::intel_gpu::ocl::ClContext>();
auto usm_tensor = gpu_context.create_tensor(element_type, shape, tensor->data);
ov_tensor = std::make_shared<ov::intel_gpu::ocl::USMTensor>(std::move(usm_tensor));
} else {
ov_tensor = std::make_shared<ov::Tensor>(element_type, shape, tensor->data);
}
return new ggml_openvino_tensor_extra(ov_tensor);
}
@@ -0,0 +1,235 @@
#pragma once
#include "ggml.h"
#include "openvino/runtime/core.hpp"
#define CL_TARGET_OPENCL_VERSION 300
#include <CL/cl.h>
#include <cstdlib>
#include <memory>
#include <openvino/core/node.hpp>
#include <openvino/runtime/remote_context.hpp>
#include <openvino/runtime/tensor.hpp>
#include <optional>
#include <string>
// ExtraQuantType enum - defines requantization target formats
// Q4_1_64: u4, group 64, *true* asymmetric (per-group scale and zero point). Note that
// Q4_0_128/Q4_0_C are symmetric despite taking the unsigned branch of quantize_q4_0 -- that branch
// pins zp to 8 with d = max/-8, which is algebraically symmetric.
enum class ExtraQuantType { F16, Q4_0_C, Q8_1_C, Q4_0_128, Q4_0_64, Q8_0_C, Q8_0_32, Q4_1_64 };
ov::Core & ov_singleton_core();
// Get the remote context for the current device (returns empty optional for CPU)
std::optional<ov::RemoteContext> ggml_openvino_get_remote_context();
// Get the compile config for the current device
const ov::AnyMap & ggml_openvino_get_compile_config();
// Get the OpenCL command queue for GPU operations (returns nullptr for CPU/NPU)
cl_command_queue ggml_openvino_get_cl_queue();
// Intel USM extension function type
typedef cl_int(CL_API_CALL * clEnqueueMemFillINTEL_fn)(cl_command_queue queue,
void * dst_ptr,
const void * pattern,
size_t pattern_size,
size_t size,
cl_uint num_events_in_wait_list,
const cl_event * event_wait_list,
cl_event * event);
typedef cl_int(CL_API_CALL * clEnqueueMemcpyINTEL_fn)(cl_command_queue queue,
cl_bool blocking,
void * dst_ptr,
const void * src_ptr,
size_t size,
cl_uint num_events_in_wait_list,
const cl_event * event_wait_list,
cl_event * event);
// Get the clEnqueueMemFillINTEL function pointer (returns nullptr if not available)
clEnqueueMemFillINTEL_fn ggml_openvino_get_clEnqueueMemFillINTEL();
// Get the clEnqueueMemcpyINTEL function pointer (returns nullptr if not available)
clEnqueueMemcpyINTEL_fn ggml_openvino_get_clEnqueueMemcpyINTEL();
// =====================================================
// Global Device Configuration (singleton)
// =====================================================
// Initialized once during backend init from GGML_OPENVINO_DEVICE env var
struct ggml_openvino_device_config {
std::string device_name = "CPU";
std::vector<std::string> available_devices;
bool is_npu = false;
bool initialized = false;
std::optional<ov::RemoteContext> remote_context;
size_t max_alloc_size = SIZE_MAX;
ov::AnyMap compile_config;
std::unordered_map<std::string, std::string> environment_variables;
cl_command_queue cl_queue = nullptr;
clEnqueueMemFillINTEL_fn cl_mem_fill_fn = nullptr;
clEnqueueMemcpyINTEL_fn cl_mem_cpy_fn = nullptr;
void init();
~ggml_openvino_device_config();
};
// Get the global device config singleton
ggml_openvino_device_config & ggml_openvino_get_device_config();
// Initialize device config (call during backend init)
void ggml_openvino_init_device_config();
// Get the device name
const std::string & ggml_openvino_get_device_name();
// Get all available physical OpenVINO devices
std::vector<std::string> ggml_openvino_get_available_devices();
// Human-readable device name, e.g. "Intel(R) AI Boost (NPU 4000)"; the device id if unavailable
std::string ggml_openvino_get_device_description(const std::string & device_name);
// Environment variable accessors. All GGML_OPENVINO_* env vars are read once
// during backend init and cached on the device config; consumers must go
// through these helpers (never call ::getenv directly) so behavior stays
// consistent and centralized.
//
// Use ggml_openvino_getenv_str() for string / path values
// (e.g. GGML_OPENVINO_DEVICE, GGML_OPENVINO_CACHE_DIR). The optional
// default_value is returned when the var is unset or empty.
//
// Use ggml_openvino_getenv_int() for boolean toggles and integer settings.
// It returns std::atoi(value) when set, otherwise default_value. For
// boolean use, `if (ggml_openvino_getenv_int(name))` is true iff the value
// is a non-zero integer (so "0" disables, "1" enables).
const char * ggml_openvino_getenv_str(const char * var, const char * default_value = nullptr);
int ggml_openvino_getenv_int(const char * var, int default_value = 0);
// Memory optimization toggles. GGML_OPENVINO_MEMORY_OPTIMIZE is an umbrella
// switch; the fine-grained env vars still override it when explicitly set.
bool ggml_openvino_reduce_compile_mem_enabled();
bool ggml_openvino_release_weights_enabled();
// Check if running on NPU
bool ggml_openvino_is_npu();
// Check if running on a GPU (GPU, GPU.0, GPU.1, ...)
bool ggml_openvino_is_gpu();
// Largest single memory object the device can allocate, SIZE_MAX when there is no known limit
size_t ggml_openvino_max_alloc_size();
// Host weight-buffer release (GGML_OPENVINO_RELEASE_WEIGHTS, GPU only).
// register: record a host weight buffer (idempotent per data pointer).
// release: madvise(MADV_DONTNEED) all registered buffers, dropping their RSS.
// released: true once release has run (used to fail-fast on post-release recompile).
void ggml_openvino_register_weight_buffer(void * data, size_t size);
void ggml_openvino_release_weight_buffers();
bool ggml_openvino_weight_buffers_released();
// Get requantization type for a tensor type (returns nullopt if no requant needed)
std::optional<ExtraQuantType> ggml_openvino_get_requant_type(const ggml_tensor * tensor, bool no_requant = false);
// =====================================================
// OpenVINO Tensor Extra Types
// =====================================================
// These types are stored in tensor->extra by the OpenVINO backend buffer.
// They allow:
// 1. Pre-built ov::Constant nodes for weights (avoiding memcpy during graph construction)
// 2. ov::Tensor wrappers for KV cache / compute tensors (for direct use with infer_request)
// Base class for OpenVINO tensor extra data
struct ggml_openvino_extra_base {
enum class Type { WEIGHT, QUANTIZED_WEIGHT, TENSOR };
Type type;
virtual ~ggml_openvino_extra_base() = default;
protected:
explicit ggml_openvino_extra_base(Type t) : type(t) {}
};
// Extra data for F16/F32/BF16 weight tensors - stores the pre-built weight node
struct ggml_openvino_weight_extra : public ggml_openvino_extra_base {
ov::Tensor weights; // The underlying weight data tensor
std::shared_ptr<ov::Node> weight_node; // Pre-built OpenVINO weight node
ggml_openvino_weight_extra(ov::Tensor w, std::shared_ptr<ov::Node> n) :
ggml_openvino_extra_base(Type::WEIGHT),
weights(std::move(w)),
weight_node(std::move(n)) {}
};
// Extra data for quantized weight tensors - stores extracted weights/scales/zp and weight node
struct ggml_openvino_quantized_weight_extra : public ggml_openvino_extra_base {
ov::Tensor weights; // U4 or U8 extracted weights
ov::Tensor scales; // F16 scales
ov::Tensor zp; // U4 or U8 zero points (same type as weights)
std::shared_ptr<ov::Node> weight_node; // Pre-built OpenVINO weight subgraph
ggml_openvino_quantized_weight_extra(ov::Tensor w, ov::Tensor s, ov::Tensor z, std::shared_ptr<ov::Node> n) :
ggml_openvino_extra_base(Type::QUANTIZED_WEIGHT),
weights(std::move(w)),
scales(std::move(s)),
zp(std::move(z)),
weight_node(std::move(n)) {}
};
// Extra data for KV cache / compute tensors - stores ov::Tensor for infer_request
struct ggml_openvino_tensor_extra : public ggml_openvino_extra_base {
std::shared_ptr<ov::Tensor> tensor; // For direct use with infer_request
explicit ggml_openvino_tensor_extra(std::shared_ptr<ov::Tensor> t) :
ggml_openvino_extra_base(Type::TENSOR),
tensor(std::move(t)) {}
};
// =====================================================
// Extracted Size Calculation for Quantized Tensors
// =====================================================
// For quantized tensors, we need extra space to store extracted weights, scales, and zero points.
// Returns the total size needed in the buffer for extracted data.
struct ggml_openvino_extracted_layout {
size_t total_size = 0; // Total bytes needed
size_t weights_offset = 0; // Offset to weights in buffer
size_t weights_size = 0; // Size of weights in bytes
size_t scales_offset = 0; // Offset to scales in buffer
size_t scales_size = 0; // Size of scales in bytes
size_t zp_offset = 0; // Offset to zero points in buffer
size_t zp_size = 0; // Size of zero points in bytes (U4 or U8)
bool is_u4; // true for U4 weights, false for U8
int64_t weights_per_block; // weights per scale/zp block
bool is_symmetric; // true for symmetric quantization
// Requantization info
bool is_requant = false; // true if this tensor needs requantization
std::optional<ExtraQuantType> requant_type; // target requant type if is_requant
};
// Calculate the buffer layout for extracted quantized data
ggml_openvino_extracted_layout ggml_openvino_get_extracted_layout(const ggml_tensor * tensor, bool use_bias = false);
ggml_openvino_tensor_extra * ggml_openvino_create_tensor_extra(const ggml_tensor * tensor, bool is_remote);
// Check if a tensor's buffer uses remote (device) memory (e.g. GPU USM)
bool ggml_openvino_buffer_is_remote(const ggml_tensor * tensor);
// Register an extra with the tensor's OpenVINO buffer context for proper lifetime management.
// This sets tensor->extra and tracks the extra in the buffer context for cleanup.
void ggml_openvino_buffer_register_extra(ggml_tensor * tensor, ggml_openvino_extra_base * extra);
// =====================================================
// OpenVINO Backend Context and Interface
// =====================================================
struct ggml_backend_openvino_context {
int device = 0;
std::string name = "OpenVINO";
std::string description = "OpenVINO Backend Context";
std::shared_ptr<void> runtime_context = nullptr;
ggml_backend_openvino_context() = default;
};
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,58 @@
#pragma once
#include "ggml-openvino-extra.h" // For ExtraQuantType
#include "ggml.h"
#include <openvino/core/node_output.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/runtime/tensor.hpp>
static constexpr size_t GGML_QUANTIZATION_GROUP_SIZE = 32;
inline const char * extra_quant_type_name(ExtraQuantType t) {
switch (t) {
case ExtraQuantType::F16:
return "F16";
case ExtraQuantType::Q4_0_C:
return "Q4_0_C";
case ExtraQuantType::Q4_0_128:
return "Q4_0_128";
case ExtraQuantType::Q8_0_C:
return "Q8_0_C";
case ExtraQuantType::Q8_0_32:
return "Q8_0_32";
case ExtraQuantType::Q8_1_C:
return "Q8_1_C";
case ExtraQuantType::Q4_0_64:
return "Q4_0_64";
case ExtraQuantType::Q4_1_64:
return "Q4_1_64";
default:
return "unknown";
}
}
// Result from process_weight_tensor containing the weight node and tensors.
// For quantized weights, also contains the extracted layout and scale/zp tensors.
struct OvWeight {
std::shared_ptr<ov::Node> weight_node;
ggml_openvino_extracted_layout layout; // Only meaningful for quantized (layout.total_size > 0)
ov::Tensor weights;
ov::Tensor scales;
ov::Tensor zp;
bool is_quantized() const { return layout.scales_size > 0; }
};
// Process weight tensor and create an OpenVINO weight node
// Handles F16/F32/BF16 and quantized weights, with optional requantization
// If output_base_ptr is nullptr, allocates internal buffers (for decoder use)
// If output_base_ptr is provided, uses pre-allocated buffers at specified offsets (for backend buffer use)
// Returns OvWeight with the weight node and optional quantized tensors
OvWeight process_weight_tensor(
const ggml_tensor * tensor,
const void * data, // Source data pointer (may differ from tensor->data)
void * output_base_ptr = nullptr, // Base pointer for output buffers (or nullptr for internal allocation)
bool use_bias = false); // Use an exact f16 zero point (vs. a rounded integer one);
// always used for for_gather_matmul (3D MoE expert) weights
// regardless of this flag, and also settable explicitly for
// test-backend-ops.
@@ -0,0 +1,484 @@
#include "model-cache.h"
#include "ggml-backend-impl.h"
#include "ggml-backend.h"
#include "ggml-impl.h"
#include "ggml-openvino-extra.h"
#include <cerrno>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <limits>
#include <sstream>
#include <openvino/core/version.hpp>
#include <string>
#include <sys/stat.h>
#include <sys/types.h>
#include <vector>
#if defined(_WIN32)
# define WIN32_LEAN_AND_MEAN
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <windows.h>
# include <psapi.h>
# include <direct.h>
# include <process.h>
#else
# include <unistd.h>
#endif
#ifdef __linux__
# include <sys/sysmacros.h>
#endif
namespace {
// 64-bit FNV-1a, the mixing primitive for all fingerprints here.
inline uint64_t fnv1a(uint64_t h, const void * data, size_t n) {
const uint8_t * p = static_cast<const uint8_t *>(data);
for (size_t i = 0; i < n; ++i) {
h ^= p[i];
h *= 0x100000001b3ull;
}
return h;
}
inline uint64_t fnv1a_u64(uint64_t h, uint64_t v) {
return fnv1a(h, &v, sizeof(v));
}
constexpr uint64_t FNV_OFFSET = 0xcbf29ce484222325ull;
// Fallback when source-file identity is unavailable outside cache-only mode.
constexpr size_t WEIGHT_SAMPLE_BYTES = 4096;
// Is this src a model weight, mirroring create_weight_nodes()'s selection:
// non-view tensor whose buffer is USAGE_WEIGHTS or whose type is quantized.
bool is_weight_src(const ggml_tensor * src) {
if (src == nullptr || src->view_src != nullptr || src->buffer == nullptr) {
return false;
}
return src->buffer->usage == GGML_BACKEND_BUFFER_USAGE_WEIGHTS || ggml_is_quantized(src->type);
}
// Weight metadata and source identity; do not read repacked or unloaded buffers.
uint64_t weight_fingerprint(const ggml_tensor * t) {
uint64_t h = FNV_OFFSET;
h = fnv1a(h, t->name, strlen(t->name));
for (int i = 0; i < GGML_MAX_DIMS; ++i) {
h = fnv1a_u64(h, static_cast<uint64_t>(t->ne[i]));
}
h = fnv1a_u64(h, static_cast<uint64_t>(t->type));
const size_t nbytes = ggml_nbytes(t);
h = fnv1a_u64(h, nbytes);
return fnv1a_u64(h, ggml_backend_openvino_weight_fingerprint(t));
}
// Walk the cgraph and invoke fn(weight_tensor) for each distinct weight, in node
// order. De-duplicates by tensor pointer so a weight used by several nodes is
// fingerprinted once, deterministically.
template <typename F>
void for_each_weight(const ggml_cgraph * cgraph, F && fn) {
std::vector<const ggml_tensor *> seen;
for (int i = 0; i < cgraph->n_nodes; ++i) {
const ggml_tensor * node = cgraph->nodes[i];
for (int s = 0; s < GGML_MAX_SRC; ++s) {
const ggml_tensor * src = node->src[s];
if (!is_weight_src(src)) {
continue;
}
bool dup = false;
for (const auto * p : seen) {
if (p == src) {
dup = true;
break;
}
}
if (dup) {
continue;
}
seen.push_back(src);
fn(src);
}
}
}
std::string ov_version_string() {
const ov::Version v = ov::get_openvino_version();
return std::string(v.buildNumber ? v.buildNumber : "unknown");
}
std::string hex64(uint64_t v) {
char buf[17];
snprintf(buf, sizeof(buf), "%016llx", static_cast<unsigned long long>(v));
return std::string(buf);
}
// Portable mkdir for a single path component. Returns true if the directory
// exists after the call (created now or already present).
bool make_dir(const std::string & path) {
#if defined(_WIN32)
int rc = _mkdir(path.c_str());
#else
int rc = ::mkdir(path.c_str(), 0755);
#endif
if (rc == 0 || errno == EEXIST) {
return true;
}
return false;
}
// Create `path` and any missing parents (like `mkdir -p`). Best-effort:
// returns true only if the full directory exists afterwards.
bool make_dirs(const std::string & path) {
if (path.empty()) {
return false;
}
std::string acc;
for (size_t i = 0; i < path.size(); ++i) {
const char c = path[i];
acc.push_back(c);
const bool sep = (c == '/'
#if defined(_WIN32)
|| c == '\\'
#endif
);
// Create each intermediate component (skip a leading "/" root).
if (sep && acc.size() > 1) {
std::string component = acc.substr(0, acc.size() - 1);
if (!make_dir(component)) {
return false;
}
}
}
return make_dir(path);
}
} // namespace
bool ggml_openvino_model_cache_only() {
return ggml_openvino_getenv_int("GGML_OPENVINO_COMPILED_MODEL_CACHE_ONLY") != 0;
}
static const char * cache_settings[] = {
"GGML_OPENVINO_REQUANT_KQUANT",
"GGML_OPENVINO_NATIVE_SOFTPLUS",
"GGML_OPENVINO_DISABLE_KV_SLICE",
"GGML_OPENVINO_MANUAL_GQA_ATTN",
"GGML_OPENVINO_STATEFUL_EXECUTION",
"GGML_OPENVINO_DISABLE_KV_STATE_RELAYOUT",
"GGML_OPENVINO_DISABLE_REMOTE_OUTPUTS",
"GGML_OPENVINO_REDUCE_COMPILE_MEM",
"GGML_OPENVINO_MEMORY_OPTIMIZE",
"GGML_OPENVINO_PROFILING",
};
void ggml_openvino_model_cache_init() {
const bool cache_only = ggml_openvino_model_cache_only();
const std::string dir = ggml_openvino_model_cache_dir();
if (dir.empty()) {
if (cache_only) {
GGML_ABORT("ggml-openvino: cache-only mode requires GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR");
}
return;
}
if (cache_only && (ggml_openvino_is_npu() || ggml_openvino_getenv_int("GGML_OPENVINO_FORCE_STATIC") ||
ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_CACHE") ||
ggml_openvino_getenv_int("GGML_OPENVINO_ENABLE_FALLBACK"))) {
GGML_ABORT("ggml-openvino: cache-only mode requires dynamic CPU/GPU execution with caching and without fallback");
}
#if !defined(__linux__) && !defined(_WIN32)
if (cache_only) {
GGML_ABORT("ggml-openvino: cache-only mmap identification requires Linux or Windows");
}
#endif
if (cache_only) {
auto & config = ggml_openvino_get_device_config();
config.environment_variables.erase("GGML_OPENVINO_SPILL_DIR");
config.environment_variables["GGML_OPENVINO_RELEASE_WEIGHTS"] = "0";
}
}
uint64_t ggml_openvino_source_fingerprint(const void * data, size_t size, std::vector<ggml_openvino_source_mapping> & mappings) {
const uintptr_t address = reinterpret_cast<uintptr_t>(data);
auto contains = [&](const ggml_openvino_source_mapping & m) {
return address >= m.begin && address < m.end && size <= m.end - address;
};
auto fingerprint = [&](const ggml_openvino_source_mapping & m) {
return fnv1a_u64(m.identity, m.offset + address - m.begin);
};
for (const auto & m : mappings) {
if (contains(m)) {
return fingerprint(m);
}
}
#ifdef __linux__
std::ifstream maps("/proc/self/maps");
std::string line;
while (std::getline(maps, line)) {
unsigned long long begin, end, offset, inode;
unsigned int dev_major, dev_minor;
char permissions[5];
int path_start = 0;
if (sscanf(line.c_str(), "%llx-%llx %4s %llx %x:%x %llu %n", &begin, &end, permissions,
&offset, &dev_major, &dev_minor, &inode, &path_start) != 7 || inode == 0) {
continue;
}
ggml_openvino_source_mapping m{uintptr_t(begin), uintptr_t(end), offset, FNV_OFFSET};
if (!contains(m)) {
continue;
}
struct stat st;
const std::string path = line.substr(path_start);
if (stat(path.c_str(), &st) != 0 || !S_ISREG(st.st_mode) || uint64_t(st.st_ino) != inode ||
major(st.st_dev) != dev_major || minor(st.st_dev) != dev_minor) {
break;
}
m.identity = fnv1a_u64(m.identity, st.st_dev);
m.identity = fnv1a_u64(m.identity, st.st_ino);
m.identity = fnv1a_u64(m.identity, st.st_size);
m.identity = fnv1a_u64(m.identity, st.st_mtim.tv_sec);
m.identity = fnv1a_u64(m.identity, st.st_mtim.tv_nsec);
m.identity = fnv1a_u64(m.identity, st.st_ctim.tv_sec);
m.identity = fnv1a_u64(m.identity, st.st_ctim.tv_nsec);
mappings.push_back(m);
return fingerprint(m);
}
#elif defined(_WIN32)
MEMORY_BASIC_INFORMATION memory;
if (VirtualQuery(data, &memory, sizeof(memory)) == sizeof(memory) && memory.Type == MEM_MAPPED) {
std::wstring name(MAX_PATH, L'\0');
DWORD length = 0;
while (name.size() <= 32768) {
length = GetMappedFileNameW(GetCurrentProcess(), const_cast<void *>(data), name.data(), static_cast<DWORD>(name.size()));
if (length == 0 || length < name.size() - 1) {
break;
}
name.resize(name.size() * 2);
}
if (length > 0 && length < name.size() - 1) {
name.resize(length);
const std::wstring path = L"\\\\?\\GLOBALROOT" + name;
HANDLE file = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file != INVALID_HANDLE_VALUE) {
BY_HANDLE_FILE_INFORMATION info;
FILE_BASIC_INFO basic;
const bool valid = GetFileInformationByHandle(file, &info) &&
GetFileInformationByHandleEx(file, FileBasicInfo, &basic, sizeof(basic));
CloseHandle(file);
if (valid) {
const uint64_t file_size = (uint64_t(info.nFileSizeHigh) << 32) | info.nFileSizeLow;
const uintptr_t begin = reinterpret_cast<uintptr_t>(memory.AllocationBase);
if (file_size <= std::numeric_limits<uintptr_t>::max() - begin) {
ggml_openvino_source_mapping m{begin, begin + static_cast<uintptr_t>(file_size), 0,
fnv1a(FNV_OFFSET, "win32", 5)};
if (contains(m)) {
m.identity = fnv1a_u64(m.identity, info.dwVolumeSerialNumber);
m.identity = fnv1a_u64(m.identity, (uint64_t(info.nFileIndexHigh) << 32) | info.nFileIndexLow);
m.identity = fnv1a_u64(m.identity, file_size);
m.identity = fnv1a_u64(m.identity, (uint64_t(info.ftLastWriteTime.dwHighDateTime) << 32) |
info.ftLastWriteTime.dwLowDateTime);
m.identity = fnv1a_u64(m.identity, static_cast<uint64_t>(basic.ChangeTime.QuadPart));
mappings.push_back(m);
return fingerprint(m);
}
}
}
}
}
}
#endif
if (ggml_openvino_model_cache_only()) {
GGML_ABORT("ggml-openvino: could not identify mapped GGUF weight; use --load-mode mmap");
}
uint64_t h = FNV_OFFSET;
const size_t head = std::min(size, WEIGHT_SAMPLE_BYTES);
h = fnv1a(h, data, head);
if (size > head) {
const size_t tail = std::min(size - head, WEIGHT_SAMPLE_BYTES);
h = fnv1a(h, static_cast<const uint8_t *>(data) + size - tail, tail);
}
return h;
}
std::string ggml_openvino_model_cache_dir() {
const char * dir = ggml_openvino_getenv_str("GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR");
if (!dir || strlen(dir) == 0) {
return std::string();
}
std::string path(dir);
if (ggml_openvino_model_cache_only()) {
return path;
}
// Create the cache directory (and parents) on first use so callers don't
// have to pre-create it; a missing dir would otherwise silently disable the
// cache (manifest/blob writes fail with no directory to write into).
if (!make_dirs(path)) {
GGML_LOG_WARN("ggml-openvino: could not create model cache dir '%s' (errno=%d); caching disabled\n",
path.c_str(), errno);
return std::string();
}
return path;
}
std::string ggml_openvino_model_cache_temp_path(const std::string & path) {
#ifdef _WIN32
const int pid = _getpid();
#else
const int pid = getpid();
#endif
return path + ".tmp." + std::to_string(pid) + "." + std::to_string(ggml_time_us());
}
uint64_t ggml_openvino_model_fingerprint(const ggml_cgraph * cgraph,
const std::string & device,
bool fa,
const int32_t * rope_params,
int rope_len,
uint64_t extra_cfg,
const std::string & graph_signature) {
uint64_t h = FNV_OFFSET;
h = fnv1a_u64(h, 2);
h = fnv1a(h, graph_signature.data(), graph_signature.size());
for (const char * name : cache_settings) {
const char * value = ggml_openvino_getenv_str(name, "");
h = fnv1a(h, value, strlen(value) + 1);
}
if (const char * debug_nodes = ggml_openvino_getenv_str("GGML_OPENVINO_DEBUG_NODE")) {
h = fnv1a(h, "GGML_OPENVINO_DEBUG_NODE", sizeof("GGML_OPENVINO_DEBUG_NODE"));
h = fnv1a(h, debug_nodes, strlen(debug_nodes) + 1);
}
if (ggml_openvino_is_gpu() && ggml_openvino_getenv_int("GGML_OPENVINO_MOE_OP", 1) == 0) {
h = fnv1a(h, "GGML_OPENVINO_MOE_OP=0", sizeof("GGML_OPENVINO_MOE_OP=0"));
}
// Topology: node count + each node's op and name (cheap, and distinguishes
// graphs that share weights but differ structurally).
h = fnv1a_u64(h, static_cast<uint64_t>(cgraph->n_nodes));
for (int i = 0; i < cgraph->n_nodes; ++i) {
const ggml_tensor * node = cgraph->nodes[i];
h = fnv1a_u64(h, static_cast<uint64_t>(node->op));
h = fnv1a(h, node->name, strlen(node->name));
}
// Weights: the model identity.
for_each_weight(cgraph, [&](const ggml_tensor * t) { h = fnv1a_u64(h, weight_fingerprint(t)); });
// Device, model parameters, and backend configuration.
h = fnv1a(h, device.data(), device.size());
h = fnv1a_u64(h, fa ? 1u : 0u);
if (rope_params && rope_len > 0) {
h = fnv1a(h, rope_params, sizeof(int32_t) * static_cast<size_t>(rope_len));
}
h = fnv1a_u64(h, extra_cfg);
const std::string ver = ov_version_string();
h = fnv1a(h, ver.data(), ver.size());
return h;
}
std::string ggml_openvino_model_cache_blob_path(const std::string & dir, uint64_t fingerprint) {
return dir + "/" + hex64(fingerprint) + ".blob";
}
std::string ggml_openvino_model_cache_manifest_path(const std::string & dir, uint64_t fingerprint) {
return dir + "/" + hex64(fingerprint) + ".manifest";
}
bool ggml_openvino_model_cache_write_manifest(const std::string & path,
const ggml_cgraph * cgraph,
uint64_t fingerprint,
const std::vector<std::string> & inputs,
const std::vector<std::string> & outputs) {
std::ofstream f(path, std::ios::trunc);
if (!f.is_open()) {
return false;
}
f << "fingerprint " << hex64(fingerprint) << "\n";
f << "ov_version " << ov_version_string() << "\n";
for_each_weight(cgraph, [&](const ggml_tensor * t) {
f << t->name << " " << t->ne[0] << " " << t->ne[1] << " " << t->ne[2] << " " << t->ne[3] << " "
<< static_cast<int>(t->type) << " " << hex64(weight_fingerprint(t)) << "\n";
});
f << "ports\n";
for (const auto * names : { &inputs, &outputs }) {
f << names->size() << '\n';
for (const auto & name : *names) {
f << std::quoted(name) << '\n';
}
}
return f.good();
}
bool ggml_openvino_model_cache_verify_manifest(const std::string & path,
const ggml_cgraph * cgraph,
uint64_t fingerprint,
std::vector<std::string> & inputs,
std::vector<std::string> & outputs) {
std::ifstream f(path);
if (!f.is_open()) {
return false;
}
std::string tag;
std::string val;
// header: fingerprint
if (!(f >> tag >> val) || tag != "fingerprint" || val != hex64(fingerprint)) {
return false;
}
// header: ov_version
if (!(f >> tag >> val) || tag != "ov_version" || val != ov_version_string()) {
return false;
}
// Build the expected per-weight lines from the live cgraph, then require an
// exact match (same set, same order) against the manifest.
std::vector<std::string> expected;
for_each_weight(cgraph, [&](const ggml_tensor * t) {
expected.push_back(std::string(t->name) + " " + std::to_string(t->ne[0]) + " " + std::to_string(t->ne[1]) +
" " + std::to_string(t->ne[2]) + " " + std::to_string(t->ne[3]) + " " +
std::to_string(static_cast<int>(t->type)) + " " + hex64(weight_fingerprint(t)));
});
size_t idx = 0;
std::string line;
std::getline(f, line); // consume rest of ov_version line
while (idx < expected.size() && std::getline(f, line)) {
if (line.empty()) {
continue;
}
if (idx >= expected.size() || line != expected[idx]) {
return false;
}
++idx;
}
if (idx != expected.size()) {
return false;
}
if (!std::getline(f, line)) {
return true;
}
if (line != "ports") {
return false;
}
for (auto * names : { &inputs, &outputs }) {
size_t count;
if (!(f >> count) || count > 100000) {
return false;
}
for (size_t i = 0; i < count; ++i) {
std::string name;
if (!(f >> std::quoted(name))) {
return false;
}
names->push_back(name);
}
}
f >> std::ws;
return f.eof();
}
@@ -0,0 +1,57 @@
#pragma once
// Compiled blobs include weights. Cache-only execution skips weight uploads and graph compilation.
#include "ggml.h"
#include <cstdint>
#include <string>
#include <vector>
bool ggml_openvino_model_cache_only();
void ggml_openvino_model_cache_init();
struct ggml_openvino_source_mapping {
uintptr_t begin;
uintptr_t end;
uint64_t offset;
uint64_t identity;
};
// Identify mmap weights without reading their pages. Cache mappings for one buffer lifetime.
uint64_t ggml_openvino_source_fingerprint(const void * data, size_t size, std::vector<ggml_openvino_source_mapping> & mappings);
uint64_t ggml_backend_openvino_weight_fingerprint(const ggml_tensor * tensor);
// Returns the compiled-model cache directory, or empty if unset.
std::string ggml_openvino_model_cache_dir();
std::string ggml_openvino_model_cache_temp_path(const std::string & path);
// Hash graph structure, source weight identities, configuration, and OpenVINO version.
uint64_t ggml_openvino_model_fingerprint(const ggml_cgraph * cgraph,
const std::string & device,
bool fa,
const int32_t * rope_params,
int rope_len,
uint64_t extra_cfg,
const std::string & graph_signature);
// Path to the compiled-blob file for a fingerprint (<dir>/<hex>.blob).
std::string ggml_openvino_model_cache_blob_path(const std::string & dir, uint64_t fingerprint);
// Path to the sidecar manifest (<dir>/<hex>.manifest) holding the per-weight
// fingerprints, used to re-verify a hit before trusting the blob.
std::string ggml_openvino_model_cache_manifest_path(const std::string & dir, uint64_t fingerprint);
// Record weight metadata and source identities. Returns false on I/O error.
bool ggml_openvino_model_cache_write_manifest(const std::string & path,
const ggml_cgraph * cgraph,
uint64_t fingerprint,
const std::vector<std::string> & inputs,
const std::vector<std::string> & outputs);
// Require all weight metadata and source identities to match the manifest.
bool ggml_openvino_model_cache_verify_manifest(const std::string & path,
const ggml_cgraph * cgraph,
uint64_t fingerprint,
std::vector<std::string> & inputs,
std::vector<std::string> & outputs);
@@ -0,0 +1,133 @@
#pragma once
#include <cstdint>
#include <map>
#include <openvino/core/node.hpp>
#include <openvino/core/partial_shape.hpp>
#include <openvino/core/shape.hpp>
#include <openvino/frontend/decoder.hpp>
#include <set>
#include <string>
namespace ov {
namespace frontend {
namespace ggml {
struct ModelInputInfo {
element::Type type;
PartialShape shape;
};
struct ModelExtraInputInfo {
element::Type type;
Shape shape;
int64_t value;
bool is_parameter;
};
class GgmlDecoder : public DecoderBase {
public:
virtual ov::Any get_attribute(const std::string & name) const = 0;
virtual PartialShape get_input_shape(int node_idx, const std::string & name) const = 0;
virtual std::vector<size_t> get_input_stride(int node_idx, const std::string & name) const = 0;
virtual size_t get_view_input_size(int node_idx, const std::string & name) const = 0;
virtual size_t get_view_input_offset(int node_idx, const std::string & name, size_t view_index) const = 0;
virtual size_t get_view_input_src_offset(int node_idx, const std::string & name, size_t view_index) const = 0;
virtual std::vector<size_t> get_view_input_stride(int node_idx,
const std::string & name,
size_t view_index) const = 0;
virtual std::vector<size_t> get_view_input_src_stride(int node_idx,
const std::string & name,
size_t view_index) const = 0;
virtual Shape get_view_input_ggml_shape(int node_idx, const std::string & name, size_t view_index) const = 0;
virtual Shape get_view_input_src_ggml_shape(int node_idx, const std::string & name, size_t view_index) const = 0;
virtual PartialShape get_view_input_ov_shape(int node_idx, const std::string & name, size_t view_index) const = 0;
virtual PartialShape get_view_input_src_ov_shape(int node_idx,
const std::string & name,
size_t view_index) const = 0;
virtual std::string get_view_input_name(int node_idx, const std::string & name, size_t view_index) const = 0;
virtual std::string get_view_input_src_name(int node_idx, const std::string & name, size_t view_index) const = 0;
virtual element::Type get_input_type(int node_idx, const std::string & name) const = 0;
virtual size_t get_input_size() const = 0;
virtual size_t get_input_size(int node_idx) const = 0;
virtual void get_input_node(size_t input_port_idx,
std::string & producer_name,
std::string & producer_output_port_name,
size_t & producer_output_port_index) const = 0;
virtual std::vector<std::string> get_input_names(int node_idx) const = 0;
virtual PartialShape get_output_shape(int node_idx) const = 0;
virtual element::Type get_output_type(const int node_idx) const = 0;
virtual std::vector<size_t> get_output_stride(int node_idx) const = 0;
virtual int32_t * get_input_op_params(int node_idx, const std::string & name) const = 0;
virtual int32_t * get_output_op_params(int node_idx) const = 0;
virtual size_t get_output_op_offset(int node_idx) const = 0;
virtual std::vector<std::string> get_output_names(int node_idx) const = 0;
virtual std::string get_inplace_op_src(int node_idx) const = 0;
virtual bool is_view_like_alias_of(int node_idx, const std::string & view_src_name) const = 0;
virtual const std::string & get_op_type() const = 0;
virtual const std::string & get_op_type(int node_idx) const = 0;
virtual const std::string & get_op_name() const = 0;
virtual const std::string & get_op_name(int node_idx) const = 0;
virtual void visit_subgraph(std::function<void(std::shared_ptr<GgmlDecoder>, int node_idx)> node_visitor) const = 0;
virtual int get_op_case(int node_idx) const = 0;
virtual const std::map<std::string, ModelInputInfo> & get_model_inputs() const = 0;
virtual const std::map<std::string, ModelExtraInputInfo> & get_model_extra_inputs() const = 0;
virtual const std::map<std::string, std::shared_ptr<ov::Node>> & get_model_weights() const = 0;
virtual std::set<std::string> get_model_output_names() const = 0;
virtual int32_t * get_rope_params() const = 0;
virtual bool has_mixed_rope_params() const = 0;
virtual int get_ssm_state_size() const = 0;
virtual std::map<std::string, std::string> get_kv_param_res_names() const = 0;
virtual bool is_static() const = 0;
virtual bool is_stateful() const = 0;
virtual bool is_splited_model() const = 0;
virtual int is_swa_layer(int layer) const = 0;
virtual int32_t get_op_dynamic_dim(int node_idx) const = 0;
};
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,28 @@
#include "frontend.h"
#include "input_model.h"
#include "op_table.h"
#include "translate_session.h"
#include <openvino/core/type.hpp>
namespace ov {
namespace frontend {
namespace ggml {
FrontEnd::FrontEnd() {}
std::shared_ptr<Model> FrontEnd::convert(const InputModel::Ptr & model, bool naive) {
auto ggml_model = ov::as_type_ptr<ggml::InputModel>(model);
FRONT_END_GENERAL_CHECK(ggml_model, "Invalid input model");
std::shared_ptr<Model> converted_model;
const auto & supported_ops = get_supported_ops();
{
TranslateSession translate_session(model, supported_ops, naive);
converted_model = translate_session.get_converted_model();
}
return converted_model;
}
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,22 @@
// Copyright (C) 2018-2024 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#pragma once
#include <openvino/frontend/frontend.hpp>
namespace ov {
namespace frontend {
namespace ggml {
class FrontEnd {
public:
FrontEnd();
static std::shared_ptr<Model> convert(const InputModel::Ptr & model, bool naive = false);
};
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,17 @@
#include "input_model.h"
#include "decoder.h"
namespace ov {
namespace frontend {
namespace ggml {
InputModel::InputModel(const std::shared_ptr<GgmlDecoder> & gdecoder) : m_decoder(gdecoder) {}
const std::shared_ptr<GgmlDecoder> & InputModel::get_model_decoder() const {
return m_decoder;
}
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,29 @@
#pragma once
#include "decoder.h"
#include <openvino/frontend/input_model.hpp>
namespace ov {
namespace frontend {
namespace ggml {
class FrontEnd;
class GgmlDecoder;
using ov::frontend::ggml::GgmlDecoder;
class InputModel : public ov::frontend::InputModel {
friend class ::ov::frontend::ggml::FrontEnd;
public:
explicit InputModel(const std::shared_ptr<GgmlDecoder> & gdecoder);
const std::shared_ptr<GgmlDecoder> & get_model_decoder() const;
private:
std::shared_ptr<GgmlDecoder> m_decoder;
};
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,180 @@
#pragma once
#include "decoder.h"
#include <cstdint>
#include <openvino/frontend/node_context.hpp>
#include <string>
namespace ov {
namespace frontend {
namespace ggml {
class TranslateSession;
typedef std::map<std::string, Output<Node>> TensorMap;
class NodeContext : public frontend::NodeContext {
public:
NodeContext(const std::shared_ptr<GgmlDecoder> & decoder,
std::shared_ptr<TensorMap> & tensor_map,
int node_idx,
TranslateSession * translate_session = nullptr) :
ov::frontend::NodeContext(decoder->get_op_type(node_idx)),
m_decoder(decoder),
m_tensor_map(tensor_map),
m_node_idx(node_idx),
m_translate_session(translate_session) {
m_input_names = decoder->get_input_names(m_node_idx);
m_output_names = decoder->get_output_names(m_node_idx);
}
TranslateSession * get_translate_session() const { return m_translate_session; }
const std::vector<std::string> & get_input_names() const { return m_input_names; }
const std::vector<std::string> & get_output_names() const { return m_output_names; }
size_t get_input_size() const override { return m_decoder->get_input_size(m_node_idx); }
ov::element::Type get_input_type(size_t index) const {
return m_decoder->get_input_type(m_node_idx, m_input_names[index]);
}
PartialShape get_input_shape(size_t input_index) const {
return m_decoder->get_input_shape(m_node_idx, m_input_names[input_index]);
}
std::vector<size_t> get_input_stride(size_t index) const {
return m_decoder->get_input_stride(m_node_idx, m_input_names[index]);
}
std::string get_output_name() const { return m_output_names[0]; }
PartialShape get_output_shape() const { return m_decoder->get_output_shape(m_node_idx); }
int32_t * get_input_op_params(size_t index) const {
return m_decoder->get_input_op_params(m_node_idx, m_input_names[index]);
}
size_t get_view_input_size(size_t index) const {
return m_decoder->get_view_input_size(m_node_idx, m_input_names[index]);
}
size_t get_view_input_offset(size_t index, size_t view_index) const {
return m_decoder->get_view_input_offset(m_node_idx, m_input_names[index], view_index);
}
size_t get_view_input_src_offset(size_t index, size_t view_index) const {
return m_decoder->get_view_input_src_offset(m_node_idx, m_input_names[index], view_index);
}
std::vector<size_t> get_view_input_stride(size_t index, size_t view_index) const {
return m_decoder->get_view_input_stride(m_node_idx, m_input_names[index], view_index);
}
std::vector<size_t> get_view_input_src_stride(size_t index, size_t view_index) const {
return m_decoder->get_view_input_src_stride(m_node_idx, m_input_names[index], view_index);
}
ov::Shape get_view_input_ggml_shape(size_t index, size_t view_index) const {
return m_decoder->get_view_input_ggml_shape(m_node_idx, m_input_names[index], view_index);
}
ov::Shape get_view_input_src_ggml_shape(size_t index, size_t view_index) const {
return m_decoder->get_view_input_src_ggml_shape(m_node_idx, m_input_names[index], view_index);
}
ov::PartialShape get_view_input_ov_shape(size_t index, size_t view_index) const {
return m_decoder->get_view_input_ov_shape(m_node_idx, m_input_names[index], view_index);
}
ov::PartialShape get_view_input_src_ov_shape(size_t index, size_t view_index) const {
return m_decoder->get_view_input_src_ov_shape(m_node_idx, m_input_names[index], view_index);
}
std::string get_view_input_name(size_t index, size_t view_index) const {
return m_decoder->get_view_input_name(m_node_idx, m_input_names[index], view_index);
}
std::string get_view_input_src_name(size_t index, size_t view_index) const {
return m_decoder->get_view_input_src_name(m_node_idx, m_input_names[index], view_index);
}
int32_t get_op_dynamic_dim() const { return m_decoder->get_op_dynamic_dim(m_node_idx); }
int32_t * get_output_op_params() const { return m_decoder->get_output_op_params(m_node_idx); }
size_t get_output_op_offset() const { return m_decoder->get_output_op_offset(m_node_idx); }
ov::element::Type get_output_type() const { return m_decoder->get_output_type(m_node_idx); }
std::vector<size_t> get_output_stride() const { return m_decoder->get_output_stride(m_node_idx); }
Output<Node> get_input(int idx) const override {
// Check if this input is a VIEW
size_t view_input_size = m_decoder->get_view_input_size(m_node_idx, m_input_names[idx]);
if (view_input_size > 0) {
// This is a VIEW input, get the base tensor name (last element in the chain)
std::string base_name =
m_decoder->get_view_input_src_name(m_node_idx, m_input_names[idx], view_input_size - 1);
// Check if the VIEW has been resolved (translate_view produced a Slice)
auto view_it = m_tensor_map->find(m_input_names[idx]);
if (!base_name.empty() && view_it != m_tensor_map->end()) {
auto base_it = m_tensor_map->find(base_name);
// A multi-output translator can publish a VIEW directly without materializing
// its packed parent (GatedDeltaNet attention/state). In that case the VIEW is the
// authoritative value. The node comparison retains the existing resolved-VIEW path.
if (base_it == m_tensor_map->end() ||
view_it->second.get_node_shared_ptr() != base_it->second.get_node_shared_ptr()) {
return view_it->second;
}
return base_it->second;
}
if (!base_name.empty()) {
return m_tensor_map->at(base_name);
}
}
// Not a VIEW or failed to get base name, use the original logic
return m_tensor_map->at(m_input_names[idx]);
}
Output<Node> get_input(const std::string & name) const override {
if (m_tensor_map->find(name) == m_tensor_map->end()) {
throw std::runtime_error("'" + name + "' not found in tensor map.");
}
return m_tensor_map->at(name);
}
bool has_input(const std::string & name) const { return m_tensor_map->find(name) != m_tensor_map->end(); }
void put_shared(const std::string & name, const Output<Node> & value) const {
m_tensor_map->insert({name, value});
}
const std::string & get_name() const override { return m_decoder->get_op_name(m_node_idx); }
ov::Any get_attribute_as_any(const std::string & name) const override { return m_decoder->get_attribute(name); }
int get_op_case() const { return m_decoder->get_op_case(m_node_idx); }
bool is_static() const { return m_decoder->is_static(); }
bool is_stateful() const { return m_decoder->is_stateful(); }
int get_ssm_state_size() const { return m_decoder->get_ssm_state_size(); }
private:
std::shared_ptr<GgmlDecoder> m_decoder;
std::shared_ptr<TensorMap> & m_tensor_map;
int m_node_idx;
TranslateSession * m_translate_session;
std::vector<std::string> m_input_names;
std::vector<std::string> m_output_names;
};
using CreatorFunction = std::function<ov::OutputVector(const ov::frontend::ggml::NodeContext &)>;
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,67 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/op/add.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/reduce_sum.hpp>
#include <openvino/op/unsqueeze.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_add(const NodeContext & context) {
num_inputs_check(context, 2, 2);
if (context.get_op_case() == 1) {
// MoE expert-plane sum (see is_moe_expert_sum_add): input 1 is a VIEW plane of the
// shared base tensor `experts` = [n_embd, n_expert_used, n_tokens, 1] (ggml order) ->
// [1, n_tokens, n_expert_used, n_embd] (OV order). The whole ADD chain is equivalent to
// reducing the expert axis (OV axis 2) of that base, so bypass the chain and the
// per-plane Slices entirely.
size_t view_size = context.get_view_input_size(1);
auto base_name = context.get_view_input_src_name(1, view_size - 1);
auto base = context.get_input(base_name);
// Stateful models drop the leading batch dim, so the base is rank 3 and both axes
// below shift down by one. Take them from the actual rank: the expert axis is always
// second from last, and the token axis is re-added just before it.
const auto base_rank = base.get_partial_shape().rank();
FRONT_END_OP_CONVERSION_CHECK(base_rank.is_static() && base_rank.get_length() >= 3,
"MoE expert sum needs a static rank of at least 3");
const int64_t rank = base_rank.get_length();
auto reduced = std::make_shared<ov::op::v1::ReduceSum>(
base, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {rank - 2}), false);
auto res = std::make_shared<ov::op::v0::Unsqueeze>(
reduced, ov::op::v0::Constant::create(ov::element::i64, {1}, {rank - 3}));
return rename_outputs_with_suffix({res}, context.get_name());
}
auto input_0 = process_view_input_new(context, 0);
auto input_1 = process_view_input_new(context, 1);
// opset1::Add needs matching types (e.g. fused ADD_ADD mixes f16/f32); add in f32, cast once.
auto output_type = context.get_output_type();
if (input_0.get_element_type() != input_1.get_element_type()) {
if (input_0.get_element_type() != ov::element::f32) {
input_0 = std::make_shared<ov::op::v0::Convert>(input_0, ov::element::f32);
}
if (input_1.get_element_type() != ov::element::f32) {
input_1 = std::make_shared<ov::op::v0::Convert>(input_1, ov::element::f32);
}
}
ov::Output<ov::Node> res = std::make_shared<ov::op::v1::Add>(input_0, input_1);
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,88 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/core/node.hpp>
#include <openvino/core/node_output.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/greater_eq.hpp>
#include <openvino/op/maximum.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/unsqueeze.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
static ov::Output<ov::Node> reshape_add_id_input_to_2d(const ov::Output<ov::Node> & input,
const ov::PartialShape & input_shape,
const std::vector<int> & dims) {
const auto & actual_shape = input.get_partial_shape();
if (actual_shape.rank().is_static() && actual_shape.rank().get_length() == 2) {
return input;
}
if (input_shape.rank().is_static() && input_shape.rank().get_length() == 2) {
return input;
}
auto shape = std::make_shared<ov::op::v3::ShapeOf>(input, ov::element::i64);
return std::make_shared<ov::op::v1::Reshape>(input, get_dimensions(shape, dims), false);
}
OutputVector translate_add_id(const NodeContext & context) {
num_inputs_check(context, 3, 3);
auto input = process_view_input_new(context, 0);
auto bias = process_view_input_new(context, 1);
auto ids = process_view_input_new(context, 2);
// OpenVINO uses reversed GGML dimensions:
// input: [1, n_token, n_used, n_embd]
// bias: [1, 1, n_expert, n_embd]
// ids: [1, 1, n_token, n_used]
// Model bias constants may already be stored as [n_expert, n_embd].
bias = reshape_add_id_input_to_2d(bias, context.get_input_shape(1), {2, 3});
ids = reshape_add_id_input_to_2d(ids, context.get_input_shape(2), {2, 3});
if (ids.get_element_type() != ov::element::i32 && ids.get_element_type() != ov::element::i64) {
ids = std::make_shared<ov::op::v0::Convert>(ids, ov::element::i32);
}
// an id of -1 marks a skipped slot: gather row 0 for it and zero the bias with a mask
auto zero = ov::op::v0::Constant::create(ids.get_element_type(), ov::Shape{}, {0});
ov::Output<ov::Node> keep = std::make_shared<ov::op::v1::GreaterEqual>(ids, zero);
keep = std::make_shared<ov::op::v0::Convert>(keep, bias.get_element_type());
keep = std::make_shared<ov::op::v0::Unsqueeze>(keep, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}));
ids = std::make_shared<ov::op::v1::Maximum>(ids, zero);
auto gather_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {0});
ov::Output<ov::Node> selected_bias = std::make_shared<ov::op::v8::Gather>(bias, ids, gather_axis);
selected_bias = std::make_shared<ov::op::v1::Multiply>(selected_bias, keep);
selected_bias = std::make_shared<ov::op::v1::Reshape>(
selected_bias, std::make_shared<ov::op::v3::ShapeOf>(input, ov::element::i64), false);
if (selected_bias.get_element_type() != input.get_element_type()) {
selected_bias = std::make_shared<ov::op::v0::Convert>(selected_bias, input.get_element_type());
}
ov::Output<ov::Node> res = std::make_shared<ov::op::v1::Add>(input, selected_bias);
const auto output_type = context.get_output_type();
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,51 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include "ggml.h"
#include <openvino/frontend/exception.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/topk.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_argsort(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
const int32_t order = context.get_output_op_params()[0];
ov::op::v11::TopK::Mode mode;
switch (order) {
case GGML_SORT_ORDER_ASC:
mode = ov::op::v11::TopK::Mode::MIN;
break;
case GGML_SORT_ORDER_DESC:
mode = ov::op::v11::TopK::Mode::MAX;
break;
default:
FRONT_END_OP_CONVERSION_CHECK(false, "Unsupported GGML_OP_ARGSORT order: ", order);
}
// Stateful models drop the leading size-1 batch dim, so the expert axis is 2 there
// instead of 3 (same rank-3-vs-rank-4 split as get_rows.cpp / process_view_input).
const int axis = (context.is_stateful() && input.get_partial_shape().rank() == 3) ? 2 : 3;
auto k = std::make_shared<ov::op::v0::Squeeze>(get_dimensions(input.get_node_shared_ptr(), {axis}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
auto topk = std::make_shared<ov::op::v11::TopK>(input, k, axis, mode, ov::op::v11::TopK::SortType::SORT_VALUES,
context.get_output_type(), false);
return rename_outputs_with_suffix({topk->output(1)}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,33 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <cstring>
#include <openvino/op/clamp.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_clamp(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
const int32_t * op_params = context.get_output_op_params();
FRONT_END_CHECK_IMPLEMENTED(op_params != nullptr, "CLAMP requires output op params");
float min;
float max;
std::memcpy(&min, reinterpret_cast<const float *>(op_params) + 0, sizeof(float));
std::memcpy(&max, reinterpret_cast<const float *>(op_params) + 1, sizeof(float));
auto res = std::make_shared<ov::op::v0::Clamp>(input, min, max);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,48 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/frontend/exception.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/convert.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_concat(const NodeContext & context) {
num_inputs_check(context, 2, 2);
const int32_t * op_params = context.get_output_op_params();
FRONT_END_CHECK_IMPLEMENTED(op_params != nullptr, "CONCAT requires output op params");
const auto output_shape = context.get_output_shape();
FRONT_END_CHECK_IMPLEMENTED(output_shape.rank().is_static(), "CONCAT requires static output rank");
const auto rank = output_shape.rank().get_length();
const int32_t ggml_dim = op_params[0];
FRONT_END_CHECK_IMPLEMENTED(ggml_dim >= 0 && ggml_dim < rank, "CONCAT axis is out of range");
auto input_0 = process_view_input_new(context, 0);
auto input_1 = process_view_input_new(context, 1);
const auto output_type = context.get_output_type();
if (input_0.get_element_type() != output_type) {
input_0 = std::make_shared<ov::op::v0::Convert>(input_0, output_type);
}
if (input_1.get_element_type() != output_type) {
input_1 = std::make_shared<ov::op::v0::Convert>(input_1, output_type);
}
const auto axis = static_cast<int64_t>(rank - 1 - ggml_dim);
auto res = std::make_shared<ov::op::v0::Concat>(OutputVector{input_0, input_1}, axis);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,37 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/op/reshape.hpp>
#include <openvino/op/slice.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_cont(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto src_shape = context.get_input_shape(0).to_shape();
auto dst_shape = context.get_output_shape().to_shape();
if (context.get_op_dynamic_dim() != -1) {
dst_shape[3 - context.get_op_dynamic_dim()] = -1;
}
auto input = process_view_input_new(context, 0);
ov::Output<Node> res;
res = std::make_shared<ov::op::v1::Reshape>(
input, ov::op::v0::Constant::create(ov::element::i64, {dst_shape.size()}, dst_shape), false);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,233 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/convolution.hpp>
#include <openvino/op/group_conv.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/unsqueeze.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_conv_2d(const NodeContext & context) {
num_inputs_check(context, 2, 2);
ov::Output<Node> kernel = process_view_input_new(context, 0);
ov::Output<Node> input = process_view_input_new(context, 1);
if (kernel.get_element_type() != input.get_element_type()) {
kernel = std::make_shared<ov::op::v0::Convert>(kernel, input.get_element_type());
}
const int32_t * params = context.get_output_op_params();
const int s0 = params[0];
const int s1 = params[1];
const int p0 = params[2];
const int p1 = params[3];
const int d0 = params[4];
const int d1 = params[5];
ov::Strides strides{static_cast<size_t>(s1), static_cast<size_t>(s0)};
ov::CoordinateDiff pads_begin{static_cast<ptrdiff_t>(p1), static_cast<ptrdiff_t>(p0)};
ov::CoordinateDiff pads_end{static_cast<ptrdiff_t>(p1), static_cast<ptrdiff_t>(p0)};
ov::Strides dilations{static_cast<size_t>(d1), static_cast<size_t>(d0)};
ov::Output<Node> res = std::make_shared<ov::op::v1::Convolution>(
input, kernel, strides, pads_begin, pads_end, dilations, ov::op::PadType::EXPLICIT);
const auto output_type = context.get_output_type();
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_conv_2d_dw(const NodeContext & context) {
num_inputs_check(context, 2, 2);
ov::Output<Node> kernel = process_view_input_new(context, 0);
ov::Output<Node> input = process_view_input_new(context, 1);
if (kernel.get_element_type() != input.get_element_type()) {
kernel = std::make_shared<ov::op::v0::Convert>(kernel, input.get_element_type());
}
const int32_t * params = context.get_output_op_params();
const int s0 = params[0];
const int s1 = params[1];
const int p0 = params[2];
const int p1 = params[3];
const int d0 = params[4];
const int d1 = params[5];
// Reshape kernel from [C, 1, KH, KW] to [C, 1, 1, KH, KW] for 2D GroupConvolution
auto unsqueeze_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {1});
auto kernel_5d = std::make_shared<ov::op::v0::Unsqueeze>(kernel, unsqueeze_axis);
ov::Strides strides{static_cast<size_t>(s1), static_cast<size_t>(s0)};
ov::CoordinateDiff pads_begin{static_cast<ptrdiff_t>(p1), static_cast<ptrdiff_t>(p0)};
ov::CoordinateDiff pads_end{static_cast<ptrdiff_t>(p1), static_cast<ptrdiff_t>(p0)};
ov::Strides dilations{static_cast<size_t>(d1), static_cast<size_t>(d0)};
ov::Output<Node> res = std::make_shared<ov::op::v1::GroupConvolution>(
input, kernel_5d, strides, pads_begin, pads_end, dilations, ov::op::PadType::EXPLICIT);
const auto output_type = context.get_output_type();
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_conv_transpose_1d(const NodeContext & context) {
num_inputs_check(context, 2, 2);
ov::Output<Node> kernel = process_view_input_new(context, 0);
ov::Output<Node> input = process_view_input_new(context, 1);
if (kernel.get_element_type() != input.get_element_type()) {
kernel = std::make_shared<ov::op::v0::Convert>(kernel, input.get_element_type());
}
const int32_t * params = context.get_output_op_params();
const int s0 = params[0];
const int p0 = params[1];
const int d0 = params[2];
const auto kernel_shape = context.get_input_shape(0).to_shape(); // [1, Cin, Cout, K]
const int64_t Cin = kernel_shape[1];
const int64_t Cout = kernel_shape[2];
const int64_t K = kernel_shape[3];
const auto input_shape = context.get_input_shape(1).to_shape(); // [1, N, Cin, L]
const int64_t N = input_shape[0] * input_shape[1];
const int64_t L = input_shape[3];
auto kernel_3d = std::make_shared<ov::op::v1::Reshape>(
kernel, ov::op::v0::Constant::create(ov::element::i64, {3}, {Cin, Cout, K}), false);
auto input_3d = std::make_shared<ov::op::v1::Reshape>(
input, ov::op::v0::Constant::create(ov::element::i64, {3}, {N, Cin, L}), false);
ov::Strides strides{static_cast<size_t>(s0)};
ov::CoordinateDiff pads_begin{static_cast<ptrdiff_t>(p0)};
ov::CoordinateDiff pads_end{static_cast<ptrdiff_t>(p0)};
ov::Strides dilations{static_cast<size_t>(d0)};
auto conv_tr = std::make_shared<ov::op::v1::ConvolutionBackpropData>(
input_3d, kernel_3d, strides, pads_begin, pads_end, dilations);
const auto out_shape = context.get_output_shape().to_shape();
auto out_shape_const = ov::op::v0::Constant::create(
ov::element::i64, {4}, {static_cast<int64_t>(out_shape[0]), static_cast<int64_t>(out_shape[1]),
static_cast<int64_t>(out_shape[2]), static_cast<int64_t>(out_shape[3])});
ov::Output<Node> res = std::make_shared<ov::op::v1::Reshape>(conv_tr, out_shape_const, false);
const auto output_type = context.get_output_type();
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_conv_transpose_2d(const NodeContext & context) {
num_inputs_check(context, 2, 2);
ov::Output<Node> kernel = process_view_input_new(context, 0);
ov::Output<Node> input = process_view_input_new(context, 1);
if (kernel.get_element_type() != input.get_element_type()) {
kernel = std::make_shared<ov::op::v0::Convert>(kernel, input.get_element_type());
}
const int32_t * params = context.get_output_op_params();
const int stride = params[0];
ov::Strides strides{static_cast<size_t>(stride), static_cast<size_t>(stride)};
ov::CoordinateDiff pads_begin{0, 0};
ov::CoordinateDiff pads_end{0, 0};
ov::Strides dilations{1, 1};
ov::Output<Node> res = std::make_shared<ov::op::v1::ConvolutionBackpropData>(
input, kernel, strides, pads_begin, pads_end, dilations);
const auto output_type = context.get_output_type();
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_conv_3d(const NodeContext & context) {
num_inputs_check(context, 2, 2);
ov::Output<Node> kernel = process_view_input_new(context, 0);
ov::Output<Node> input = process_view_input_new(context, 1);
if (kernel.get_element_type() != input.get_element_type()) {
kernel = std::make_shared<ov::op::v0::Convert>(kernel, input.get_element_type());
}
const int32_t * params = context.get_output_op_params();
const int s0 = params[0];
const int s1 = params[1];
const int s2 = params[2];
const int p0 = params[3];
const int p1 = params[4];
const int p2 = params[5];
const int d0 = params[6];
const int d1 = params[7];
const int d2 = params[8];
const int c = params[9];
const int n = params[10];
const int oc = params[11];
const auto kshape = context.get_input_shape(0).to_shape(); // [c*oc, KD, KH, KW]
const int64_t KD = kshape[1];
const int64_t KH = kshape[2];
const int64_t KW = kshape[3];
const auto inshape = context.get_input_shape(1).to_shape(); // [c*n, ID, IH, IW]
const int64_t ID = inshape[1];
const int64_t IH = inshape[2];
const int64_t IW = inshape[3];
auto kernel_5d = std::make_shared<ov::op::v1::Reshape>(
kernel, ov::op::v0::Constant::create(ov::element::i64, {5}, {static_cast<int64_t>(oc), static_cast<int64_t>(c), KD, KH, KW}), false);
auto input_5d = std::make_shared<ov::op::v1::Reshape>(
input, ov::op::v0::Constant::create(ov::element::i64, {5}, {static_cast<int64_t>(n), static_cast<int64_t>(c), ID, IH, IW}), false);
ov::Strides strides{static_cast<size_t>(s2), static_cast<size_t>(s1), static_cast<size_t>(s0)};
ov::CoordinateDiff pads_begin{static_cast<ptrdiff_t>(p2), static_cast<ptrdiff_t>(p1), static_cast<ptrdiff_t>(p0)};
ov::CoordinateDiff pads_end{static_cast<ptrdiff_t>(p2), static_cast<ptrdiff_t>(p1), static_cast<ptrdiff_t>(p0)};
ov::Strides dilations{static_cast<size_t>(d2), static_cast<size_t>(d1), static_cast<size_t>(d0)};
auto conv = std::make_shared<ov::op::v1::Convolution>(
input_5d, kernel_5d, strides, pads_begin, pads_end, dilations, ov::op::PadType::EXPLICIT);
const auto out_shape = context.get_output_shape().to_shape(); // [oc*n, OD, OH, OW]
auto out_shape_const = ov::op::v0::Constant::create(
ov::element::i64, {4}, {static_cast<int64_t>(out_shape[0]), static_cast<int64_t>(out_shape[1]),
static_cast<int64_t>(out_shape[2]), static_cast<int64_t>(out_shape[3])});
ov::Output<Node> res = std::make_shared<ov::op::v1::Reshape>(conv, out_shape_const, false);
const auto output_type = context.get_output_type();
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,314 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <climits>
#include <cstdint>
#include <cstdio>
#include <memory>
#include <numeric>
#include <openvino/frontend/exception.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/negative.hpp>
#include <openvino/op/range.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/scatter_update.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/subtract.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_cpy(const NodeContext & context) {
auto op_case = context.get_op_case();
auto input_shape = context.get_input_shape(0);
auto output_shape = context.get_input_shape(1);
if (op_case == 4) {
auto src = process_view_input_new(context, 0);
auto base = context.get_input(1);
int64_t n_elems = 1;
for (const auto & dim : context.get_output_shape().to_shape()) {
n_elems *= static_cast<int64_t>(dim);
}
const auto output_stride = context.get_output_stride();
const size_t elem_size = output_stride.empty() ? context.get_output_type().size() : output_stride.back();
FRONT_END_OP_CONVERSION_CHECK(elem_size > 0, "CPY conv state view update has invalid element size");
const int64_t begin_val = static_cast<int64_t>(context.get_output_op_offset() / elem_size);
const int64_t end_val = begin_val + n_elems;
auto flat_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, 1, -1});
src = std::make_shared<ov::op::v1::Reshape>(src, flat_shape, false);
if (src.get_element_type() != context.get_output_type()) {
src = std::make_shared<ov::op::v0::Convert>(src, context.get_output_type());
}
auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto begin = ov::op::v0::Constant::create(ov::element::i64, {1}, {begin_val});
auto end = ov::op::v0::Constant::create(ov::element::i64, {1}, {end_val});
auto int_max = ov::op::v0::Constant::create(ov::element::i64, {1}, {INT_MAX});
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
auto head_part = std::make_shared<ov::op::v8::Slice>(base, zero, begin, one, axis);
auto tail_part = std::make_shared<ov::op::v8::Slice>(base, end, int_max, one, axis);
auto res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{head_part, src, tail_part}, 3);
return rename_outputs_with_suffix({res}, context.get_name());
}
// Recurrent state cache writeback into a slot block of the cache. Where the block starts is a
// runtime input, so the cached model works for any kv head and active sequence count. The
// result is the full updated cache.
// op_case 1: gated-delta-net state, op_case 2: conv state, op_case 3: defrag remainder.
if (op_case == 3) {
// With -np 1 (and generally whenever there is no defrag remainder) this GET_ROWS gathers
// zero rows: nothing to write back, and the cache is unchanged. NPU rejects zero-size
// tensors, so short-circuit instead of building a degenerate Slice/Concat chain.
bool is_empty = false;
if (input_shape.rank().is_static()) {
for (const auto & d : input_shape) {
if (d.is_static() && d.get_length() == 0) {
is_empty = true;
break;
}
}
}
if (is_empty) {
return {context.get_input(1)};
}
}
// op_case 7/8/9 are the single-slot variants; op_case 10 writes native GDN state into a
// multi-slot cache without rollback snapshots.
const bool single_slot_assign = op_case >= 7 && op_case <= 9;
const bool direct_gdn_state = op_case == 7 || op_case == 10;
int writeback_case = op_case;
if (op_case == 10) {
writeback_case = 1;
} else if (single_slot_assign) {
writeback_case = op_case - 6;
}
const std::string slot_begin_name = "rs_slot_begin_" + context.get_name();
const bool slice_assign = writeback_case >= 1 && writeback_case <= 3 &&
(single_slot_assign || context.has_input(slot_begin_name));
if (slice_assign) {
if (single_slot_assign && writeback_case == 3) {
return {context.get_input(1)};
}
const int64_t slot_axis = 2;
auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto int_max = ov::op::v0::Constant::create(ov::element::i64, {1}, {INT_MAX});
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {slot_axis});
auto feature = ov::op::v0::Constant::create(ov::element::i64, {4},
std::vector<int64_t>{1, 1, -1, output_shape[3].get_length()});
ov::Output<ov::Node> src;
ov::Output<ov::Node> begin;
if (!single_slot_assign) {
begin = context.get_input(slot_begin_name);
}
auto base = context.get_input(1);
if (writeback_case == 1) {
if (direct_gdn_state) {
// Non-rollback GDN publishes state directly as [active_slots, heads, value_dim,
// key_dim]. Flatten each active slot before replacing or updating the cache.
src = std::make_shared<ov::op::v1::Reshape>(context.get_input(0), feature, false);
} else {
// Multi-slot rollback still consumes GGML's packed [attention | state snapshots]
// layout. Slice the state block using the runtime source offset.
auto src_begin = context.get_input("rs_src_begin_" + context.get_name());
auto state_part =
std::make_shared<ov::op::v8::Slice>(context.get_input(0), src_begin, int_max, one, axis);
src = std::make_shared<ov::op::v1::Reshape>(state_part, feature, false);
}
} else if (writeback_case == 2) {
// conv_input is [previous conv state | new tokens]; the snapshot is the conv_kernel_size - 1
// columns ending at the last *valid* token. Gather (rather than Slice) keeps the output
// shape static even though the window start is a runtime value.
auto window_size = (int64_t) input_shape[3].get_length();
ov::Output<ov::Node> window;
auto col_axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
const std::string src_begin_name = "rs_src_begin_" + context.get_name();
if (context.has_input(src_begin_name)) {
auto src_begin = context.get_input(src_begin_name);
auto src_end = std::make_shared<ov::op::v1::Add>(
src_begin, ov::op::v0::Constant::create(ov::element::i64, {1}, {window_size}));
window = std::make_shared<ov::op::v8::Slice>(context.get_input(0), src_begin, src_end, one, col_axis);
} else if (context.has_input("chunk_valid_len")) {
std::vector<int64_t> offsets(window_size);
std::iota(offsets.begin(), offsets.end(), 0);
auto indices = std::make_shared<ov::op::v1::Add>(
ov::op::v0::Constant::create(ov::element::i64, {(size_t) window_size}, offsets),
context.get_input("chunk_valid_len"));
window = std::make_shared<ov::op::v8::Gather>(context.get_input(0), indices, col_axis);
} else {
auto window_begin = ov::op::v0::Constant::create(ov::element::i64, {1}, {-window_size});
window =
std::make_shared<ov::op::v8::Slice>(context.get_input(0), window_begin, int_max, one, col_axis);
}
const auto base_shape = base.get_partial_shape();
FRONT_END_OP_CONVERSION_CHECK(base_shape.rank().is_static() && base_shape.rank().get_length() == 4,
"CPY conv state cache update requires rank-4 base cache");
FRONT_END_OP_CONVERSION_CHECK(base_shape[3].is_static(),
"CPY conv state cache update requires static feature size");
FRONT_END_OP_CONVERSION_CHECK(input_shape.rank().is_static() && input_shape.rank().get_length() == 4 &&
input_shape[2].is_static() && input_shape[3].is_static(),
"CPY conv state cache update requires static source feature view");
const int64_t full_feature_size = base_shape[3].get_length();
const int64_t update_feature_size = input_shape[2].get_length() * input_shape[3].get_length();
const auto output_stride = context.get_output_stride();
const size_t elem_size = output_stride.empty() ? context.get_output_type().size() : output_stride.back();
FRONT_END_OP_CONVERSION_CHECK(elem_size > 0,
"CPY conv state cache update has invalid element size");
const int64_t feature_begin = static_cast<int64_t>(context.get_output_op_offset() / elem_size) %
full_feature_size;
const int64_t feature_end = feature_begin + update_feature_size;
FRONT_END_OP_CONVERSION_CHECK(feature_begin >= 0 && feature_end <= full_feature_size,
"CPY conv state cache update feature range is out of bounds");
auto partial_feature = ov::op::v0::Constant::create(
ov::element::i64, {4}, std::vector<int64_t>{1, 1, -1, update_feature_size});
src = std::make_shared<ov::op::v1::Reshape>(window, partial_feature, false);
if (src.get_element_type() != context.get_output_type()) {
src = std::make_shared<ov::op::v0::Convert>(src, context.get_output_type());
}
if (single_slot_assign) {
return rename_outputs_with_suffix({src}, context.get_name());
}
auto src_len = std::make_shared<ov::op::v8::Gather>(
std::make_shared<ov::op::v3::ShapeOf>(src, ov::element::i64), axis,
ov::op::v0::Constant::create(ov::element::i64, {}, {0}));
auto slot_end = std::make_shared<ov::op::v1::Add>(begin, src_len);
auto active_slots = std::make_shared<ov::op::v8::Slice>(base, begin, slot_end, one, axis);
auto feature_axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
auto feature_begin_node = ov::op::v0::Constant::create(ov::element::i64, {1}, {feature_begin});
auto feature_end_node = ov::op::v0::Constant::create(ov::element::i64, {1}, {feature_end});
auto feature_head = std::make_shared<ov::op::v8::Slice>(active_slots, zero, feature_begin_node, one,
feature_axis);
auto feature_tail = std::make_shared<ov::op::v8::Slice>(active_slots, feature_end_node, int_max, one,
feature_axis);
src = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{feature_head, src, feature_tail}, 3);
} else {
// op_case 3: gathered remainder rows already have the cache slot layout [1, 1, extra, feature]
src = context.get_input(0);
}
if (src.get_element_type() != context.get_output_type()) {
src = std::make_shared<ov::op::v0::Convert>(src, context.get_output_type());
}
if (single_slot_assign) {
return rename_outputs_with_suffix({src}, context.get_name());
}
auto src_len =
std::make_shared<ov::op::v8::Gather>(std::make_shared<ov::op::v3::ShapeOf>(src, ov::element::i64), axis,
ov::op::v0::Constant::create(ov::element::i64, {}, {0}));
auto end = std::make_shared<ov::op::v1::Add>(begin, src_len);
auto head_part = std::make_shared<ov::op::v8::Slice>(base, zero, begin, one, axis);
auto tail_part = std::make_shared<ov::op::v8::Slice>(base, end, int_max, one, axis);
auto res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{head_part, src, tail_part}, slot_axis);
return rename_outputs_with_suffix({res}, context.get_name());
}
auto input = process_view_input_new(context, 0);
if (op_case == 5 || op_case == 6) {
auto input_shape = context.get_input_shape(0);
auto output_shape = context.get_output_shape();
auto dst_ggml_shape = context.get_view_input_ggml_shape(1, 0);
auto dst_stride = context.get_view_input_stride(1, 0);
size_t offset_bytes = context.get_view_input_offset(1, 0);
auto n_state = (int64_t) context.get_input_shape(0)[3].get_length();
auto n_state_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_state});
auto kv_buf = context.get_input(1); // shape {1,1,1,N}
Output<Node> token_len_per_seq;
Output<Node> n_write_dyn;
if (context.has_input("token_len_per_seq")) {
token_len_per_seq = context.get_input("token_len_per_seq");
n_write_dyn = std::make_shared<ov::op::v1::Multiply>(token_len_per_seq, n_state_c);
} else {
n_write_dyn = ov::op::v0::Constant::create(ov::element::i64, {1}, {(int64_t) dst_ggml_shape[3]});
}
size_t elem_size = dst_stride[3];
FRONT_END_OP_CONVERSION_CHECK(elem_size > 0, "CPY KV cache view update has invalid element size");
int64_t start_elem = (int64_t) (offset_bytes / elem_size);
// op_case 5: decoder self-attention – write offset advances each step.
// op_case 6: encoder self-attn or cross-attn – offset fixed at compile time.
const bool is_decoder_self_attn = (op_case == 5);
auto ones_c = ov::op::v0::Constant::create(ov::element::i64, {3}, std::vector<int64_t>{1, 1, 1});
auto new_shape = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{ones_c, n_write_dyn}, 0);
auto reshaped = std::make_shared<ov::op::v1::Reshape>(input, new_shape, false);
auto data = std::make_shared<ov::op::v0::Convert>(reshaped, context.get_output_type());
// Indices [start_elem .. start_elem + n_write) on axis 3 of {1,1,1,N}
// For decoder self-attention the write offset advances each step, so compute it
// dynamically from the model inputs: start = (attention_size - token_len_per_seq) * n_state.
// For encoder self-attn and cross-attn the offset is fixed at graph-compile time.
ov::Output<ov::Node> start;
if (is_decoder_self_attn && context.has_input("attention_size") && context.has_input("token_len_per_seq")) {
auto attention_size_in = context.get_input("attention_size");
auto token_len_in = context.get_input("token_len_per_seq");
auto past_tokens = std::make_shared<ov::op::v1::Subtract>(attention_size_in, token_len_in);
auto new_start = std::make_shared<ov::op::v1::Multiply>(past_tokens, n_state_c);
start = std::make_shared<ov::op::v1::Add>(
new_start, ov::op::v0::Constant::create(ov::element::i64, {1}, {start_elem}));
} else {
start = ov::op::v0::Constant::create(ov::element::i64, {1}, {start_elem});
}
auto start_squeezed = std::make_shared<ov::op::v0::Squeeze>(start);
auto end = std::make_shared<ov::op::v1::Add>(start_squeezed, n_write_dyn);
auto end_squeezed = std::make_shared<ov::op::v0::Squeeze>(end);
auto step = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto step_squeezed = std::make_shared<ov::op::v0::Squeeze>(step);
auto indices =
std::make_shared<ov::op::v4::Range>(start_squeezed, end_squeezed, step_squeezed, ov::element::i64);
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
auto kv_updated = std::make_shared<ov::op::v3::ScatterUpdate>(kv_buf, indices, data, axis);
return rename_outputs_with_suffix({kv_updated}, context.get_name());
}
if (input_shape != output_shape) {
auto new_shape = ov::op::v0::Constant::create(
ov::element::i64, {static_cast<size_t>(output_shape.rank().get_length())}, output_shape.to_shape());
input = std::make_shared<ov::op::v1::Reshape>(input, new_shape, false);
}
ov::Output<Node> res;
if (context.get_input_type(0) != context.get_output_type()) {
res = std::make_shared<ov::op::v0::Convert>(input, context.get_output_type());
} else {
res = input;
}
if (res.get_node_shared_ptr() == context.get_input(0).get_node_shared_ptr()) {
return {res};
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,29 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/constant.hpp>
#include <openvino/op/cum_sum.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML cumsum computes prefix sum along dim 0 (the innermost/fastest dimension).
// In OV layout the dims are reversed: ggml [ne0, ne1, ne2, ne3] → OV [ne3, ne2, ne1, ne0],
// so ggml dim 0 maps to OV axis 3 (last axis).
OutputVector translate_cumsum(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto x = context.get_input(0);
auto axis = ov::op::v0::Constant::create(ov::element::i64, {}, {3});
auto res = std::make_shared<ov::op::v0::CumSum>(x, axis);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,37 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/constant.hpp>
#include <openvino/op/eye.hpp>
#include <openvino/op/multiply.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML DIAG takes a 1D vector (ne0, 1, ne2, ne3) and produces a diagonal matrix
// of shape (ne0, ne0, ne2, ne3).
// In OV layout (ggml [ne0, ne1, ne2, ne3] → OV [ne3, ne2, ne1, ne0]):
// input: [ne3, ne2, 1, ne0]
// output: [ne3, ne2, ne0, ne0]
// The diagonal: output[..., i, j] = input[..., 0, j] if i == j, else 0.
OutputVector translate_diag(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto x = process_view_input_new(context, 0); // OV shape: [ne3, ne2, 1, ne0]
auto n = get_dimensions(x.get_node_shared_ptr(), {3});
auto zero_diag = ov::op::v0::Constant::create(ov::element::i64, {}, {0});
auto eye = std::make_shared<ov::op::v9::Eye>(n, n, zero_diag, x.get_element_type());
auto res = std::make_shared<ov::op::v1::Multiply>(x, eye);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,138 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include "ggml.h"
#include <memory>
#include <openvino/core/type.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/sigmoid.hpp>
#include <openvino/op/swish.hpp>
#include <openvino/op/tile.hpp>
#include <openvino/op/util/precision_sensitive_attribute.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
namespace {
bool is_silu_div_pattern(const ov::Output<ov::Node> & numerator,
const ov::Output<ov::Node> & denominator,
const NodeContext & context) {
if (context.get_input_size() != 2) {
return false;
}
const auto * unary_op = reinterpret_cast<const ggml_unary_op *>(context.get_input_op_params(0));
if (unary_op == nullptr || *unary_op != GGML_UNARY_OP_SILU) {
return false;
}
const auto denom_node = denominator.get_node_shared_ptr();
if (auto swish = ov::as_type_ptr<ov::op::v4::Swish>(numerator.get_node_shared_ptr())) {
return swish->input_value(0).get_node_shared_ptr() == denom_node;
}
return false;
}
ov::Output<ov::Node> repeat_input_to_match(const NodeContext & context,
const ov::Output<ov::Node> & input,
const ov::Output<ov::Node> & target,
size_t input_index) {
const auto input_shape = context.get_input_shape(input_index);
const auto target_shape = context.get_input_shape(0);
if (input_shape == target_shape) {
return input;
}
if (input_shape.rank().is_static() && target_shape.rank().is_static()) {
const auto rank = static_cast<size_t>(input_shape.rank().get_length());
std::vector<int64_t> repeats(rank, 1);
bool needs_repeat = false;
for (size_t axis = 0; axis < rank; ++axis) {
FRONT_END_OP_CONVERSION_CHECK(input_shape[axis].is_static() && target_shape[axis].is_static(),
"DIV repeat requires static dimensions on both inputs");
const int64_t input_dim = input_shape[axis].get_length();
const int64_t target_dim = target_shape[axis].get_length();
FRONT_END_OP_CONVERSION_CHECK(input_dim > 0 && target_dim > 0 && target_dim % input_dim == 0,
"DIV input shape ", input_shape, " cannot repeat to match ", target_shape);
repeats[axis] = target_dim / input_dim;
needs_repeat = needs_repeat || repeats[axis] != 1;
}
if (!needs_repeat) {
return input;
}
auto repeats_node = ov::op::v0::Constant::create(ov::element::i64, {repeats.size()}, repeats);
return std::make_shared<ov::op::v0::Tile>(input, repeats_node);
}
auto input_shape_node = std::make_shared<ov::op::v3::ShapeOf>(input, ov::element::i64);
auto target_shape_node = std::make_shared<ov::op::v3::ShapeOf>(target, ov::element::i64);
auto repeats_node = std::make_shared<ov::op::v1::Divide>(target_shape_node, input_shape_node);
return std::make_shared<ov::op::v0::Tile>(input, repeats_node);
}
} // namespace
OutputVector translate_div(const NodeContext & context) {
num_inputs_check(context, 2, 2);
auto input_0 = process_view_input_new(context, 0);
auto input_1 = process_view_input_new(context, 1);
if (is_silu_div_pattern(input_0, input_1, context)) {
ov::Output<ov::Node> res = std::make_shared<ov::op::v0::Sigmoid>(input_1);
if (res.get_element_type() != context.get_output_type()) {
res = std::make_shared<ov::op::v0::Convert>(res, context.get_output_type());
}
return rename_outputs_with_suffix({res}, context.get_name());
}
input_1 = repeat_input_to_match(context, input_1, input_0, 1);
const auto output_type = context.get_output_type();
const bool use_f32_compute = input_0.get_element_type() != ov::element::f32 ||
input_1.get_element_type() != ov::element::f32 || output_type != ov::element::f32;
if (use_f32_compute) {
input_0 = std::make_shared<ov::op::v0::Convert>(input_0, ov::element::f32);
input_1 = std::make_shared<ov::op::v0::Convert>(input_1, ov::element::f32);
}
ov::Output<ov::Node> res = std::make_shared<ov::op::v1::Divide>(input_0, input_1);
if (use_f32_compute) {
// Keep the reciprocal/divide path in FP32. Without this hint, the GPU
// plugin can still compress the subgraph back to FP16 and overflow on
// small shexp gate values (e.g. silu(x) / x in qwen2moe).
ov::mark_as_precision_sensitive(res.get_node_shared_ptr()->input(0));
ov::mark_as_precision_sensitive(res.get_node_shared_ptr()->input(1));
}
if (res.get_element_type() != output_type) {
auto output_convert = std::make_shared<ov::op::v0::Convert>(res, output_type);
if (use_f32_compute) {
ov::mark_as_precision_sensitive(output_convert->input(0));
}
res = output_convert;
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,34 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/broadcast.hpp>
#include <openvino/op/constant.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML FILL sets all elements of a tensor to a constant value.
// The constant is stored as a float in op_params[0].
OutputVector translate_fill(const NodeContext & context) {
num_inputs_check(context, 1, 1);
float c;
memcpy(&c, context.get_output_op_params(), sizeof(float));
auto shape = context.get_input_shape(0).to_shape();
auto val = ov::op::v0::Constant::create(context.get_output_type(), {}, {c});
auto target_shape = ov::op::v0::Constant::create(ov::element::i64, {shape.size()},
std::vector<int64_t>(shape.begin(), shape.end()));
auto res = std::make_shared<ov::op::v3::Broadcast>(val, target_shape);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,242 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include "ggml-openvino/ggml-openvino-extra.h"
#include <cstddef>
#include <cstdint>
#include <memory>
#include <openvino/op/add.hpp>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/scaled_dot_product_attention.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/softmax.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <string>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
static ov::Output<ov::Node> reshape_flat_kv(const ov::Output<ov::Node> & kv_flat,
size_t view_offset_bytes,
size_t nb1_bytes,
int64_t n_head,
int64_t head_size,
const ov::Output<ov::Node> & attention_size) {
int64_t n_state = n_head * head_size;
int64_t layer_start_elem = (int64_t) (view_offset_bytes / (nb1_bytes / n_state));
// Dynamic slice: [layer_start_elem, layer_start_elem + n_kv * n_state)
auto start_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {layer_start_elem});
auto n_state_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_state});
// end = start + attention_size * n_state (both static + dynamic)
auto kv_len_elems = std::make_shared<ov::op::v1::Multiply>(attention_size, n_state_c);
auto end_c = std::make_shared<ov::op::v1::Add>(start_c, kv_len_elems);
auto step_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto axis_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
auto sliced = std::make_shared<ov::op::v8::Slice>(kv_flat, start_c, end_c, step_c, axis_c);
// KV cache is laid out as {n_kv, n_head, head_size} in memory
// Reshape to {1, n_kv, n_head, head_size}, then transpose to {1, n_head, n_kv, head_size}
// as required by SDPA.
auto one_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto n_head_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_head});
auto head_size_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {head_size});
// reshape: {n_kv*n_state} -> {1, n_kv, n_head, head_size}
auto new_shape =
std::make_shared<ov::op::v0::Concat>(ov::OutputVector{one_c, attention_size, n_head_c, head_size_c}, 0);
auto reshaped = std::make_shared<ov::op::v1::Reshape>(sliced, new_shape, false);
// transpose: {1, n_kv, n_head, head_size} -> {1, n_head, n_kv, head_size}
auto perm = ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 2, 1, 3});
auto ret = std::make_shared<ov::op::v1::Transpose>(reshaped, perm);
return ret;
}
OutputVector translate_flash_attn_ext(const NodeContext & context) {
num_inputs_check(context, 3, 4);
const bool has_mask = context.get_input_size() == 4;
auto q_f32 = context.get_input(0);
auto k = context.get_input(1);
auto v = context.get_input(2);
const int op_case = context.get_op_case();
if (op_case == 1 || op_case == 2) {
int64_t n_state_head = (int64_t) context.get_view_input_ggml_shape(1, 0)[3];
int64_t n_head = (int64_t) context.get_view_input_ggml_shape(1, 0)[1];
size_t nb1 = context.get_view_input_stride(1, 0)[2];
size_t offset = context.get_view_input_offset(1, 0);
ov::Output<ov::Node> attention_size;
if (op_case == 1) {
attention_size = context.get_input("attention_size");
} else {
attention_size = context.get_input("attention_size_static");
}
k = reshape_flat_kv(k, offset, nb1, n_head, n_state_head, attention_size);
v = reshape_flat_kv(v, offset, nb1, n_head, n_state_head, attention_size);
}
float * params = reinterpret_cast<float *>(context.get_output_op_params());
float scale = params[0];
// float max_bias = params[1];
// float logit_softcap = params[2];
auto q = std::make_shared<ov::op::v0::Convert>(q_f32, ov::element::f16);
auto scale_node = std::make_shared<ov::op::v0::Constant>(ov::element::f16, ov::Shape{}, std::vector<float>{scale});
ov::Output<ov::Node> res;
// For stateful
ov::Output<ov::Node> mask;
if (has_mask) {
mask = context.get_input(3);
std::string mask_name = "KQ_mask_sliced";
if (context.get_input_names()[3].find("swa") != std::string::npos) {
mask_name = "KQ_mask_swa_sliced";
}
if (context.has_input(mask_name)) {
mask = context.get_input(mask_name);
}
if (mask.get_element_type() != ov::element::f16) {
mask = std::make_shared<ov::op::v0::Convert>(mask, ov::element::f16);
}
}
//auto tile_kv = [&](int64_t num_heads, int64_t num_heads_kv, int64_t head_size, ov::Output<Node> kv) {
// int64_t factor = num_heads / num_heads_kv;
// if (factor > 1 && num_heads_kv > 1) {
auto q_shape = context.get_input_shape(0).to_shape();
auto k_shape = context.get_input_shape(1).to_shape();
const int64_t num_heads = q_shape[1];
const int64_t num_heads_kv = k_shape[1];
const int64_t head_size = q_shape[3];
const int64_t factor = num_heads / num_heads_kv;
// Manual GQA attention: enabled by default on GPU in stateless mode.
// Set GGML_OPENVINO_MANUAL_GQA_ATTN to a positive value (e.g. 1) to force-enable,
// or to 0 to force-disable. Unset falls back to the device-based default.
static const bool manual_gqa_enabled = []() {
const char * env = ggml_openvino_getenv_str("GGML_OPENVINO_MANUAL_GQA_ATTN");
if (env != nullptr) {
return ggml_openvino_getenv_int("GGML_OPENVINO_MANUAL_GQA_ATTN") > 0;
}
return ggml_openvino_is_gpu();
}();
const bool use_manual_gqa_attention =
manual_gqa_enabled && factor > 1 && num_heads_kv > 1 && !context.is_stateful();
if (use_manual_gqa_attention) {
// Q, K, V arrive as [B, n_heads(_kv), S, head_size], where B is the active
// batch (n_seq_active) and may be > 1 (llama-perplexity, llama-server -np > 1)
// or dynamic. Reshape to
// K_r: [B, num_heads_kv, 1, S, head_size]
// Q_r: [B, num_heads_kv, factor, S_q, head_size]
// and let MatMul broadcast across the factor dim without materialising
// an expanded K/V. The leading 0 + special_zero=true copies B at runtime,
// so this is correct for B == 1, B > 1, and dynamic B alike. Only the head
// dims and head_size are baked in as literals; the sequence dim stays -1.
auto k_5d_shape = ov::op::v0::Constant::create(ov::element::i64, {5},
std::vector<int64_t>{0, num_heads_kv, 1, -1, head_size});
auto v_5d_shape = ov::op::v0::Constant::create(ov::element::i64, {5},
std::vector<int64_t>{0, num_heads_kv, 1, -1, head_size});
auto q_5d_shape = ov::op::v0::Constant::create(ov::element::i64, {5},
std::vector<int64_t>{0, num_heads_kv, factor, -1, head_size});
auto k_r = std::make_shared<ov::op::v1::Reshape>(k, k_5d_shape, true);
auto v_r = std::make_shared<ov::op::v1::Reshape>(v, v_5d_shape, true);
auto q_r = std::make_shared<ov::op::v1::Reshape>(q, q_5d_shape, true);
// QK^T → [B, num_heads_kv, factor, S_q, S_k]
auto qk = std::make_shared<ov::op::v0::MatMul>(q_r, k_r, /*tA=*/false, /*tB=*/true);
auto qk_scaled = std::make_shared<ov::op::v1::Multiply>(qk, scale_node);
// Mask arrives as [B, 1, S_q, S_k]. Unsqueeze a factor axis at position 2 to
// get [B, 1, 1, S_q, S_k], which NUMPY-broadcasts cleanly against the
// [B, num_heads_kv, factor, S_q, S_k] scores: B==B, then 1→num_heads_kv and
// 1→factor on the head dims.
ov::Output<ov::Node> qk_masked;
if (has_mask) {
auto mask_unsq1 =
std::make_shared<ov::op::v0::Unsqueeze>(mask, ov::op::v0::Constant::create(ov::element::i64, {1}, {2}));
qk_masked = std::make_shared<ov::op::v1::Add>(qk_scaled, mask_unsq1);
} else {
qk_masked = qk_scaled;
}
auto softmax = std::make_shared<ov::op::v8::Softmax>(qk_masked, /*axis=*/-1);
// softmax @ V → [B, num_heads_kv, factor, S_q, head_size]
auto attn = std::make_shared<ov::op::v0::MatMul>(softmax, v_r);
// Reshape back to [B, num_heads, S_q, head_size] (combine num_heads_kv * factor).
// Leading 0 + special_zero=true copies B at runtime.
auto out_4d_shape =
ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{0, num_heads, -1, head_size});
auto out_4d = std::make_shared<ov::op::v1::Reshape>(attn, out_4d_shape, true);
// The standard SDPA path's downstream is Transpose(0,2,1,3) → Convert(f32).
// Replicate it here so callers see the same output layout/dtype.
res = std::make_shared<ov::op::v1::Transpose>(
out_4d, ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 2, 1, 3}));
res = std::make_shared<ov::op::v0::Convert>(res, ov::element::f32);
return rename_outputs_with_suffix({res}, context.get_name());
}
// Default path: explicit Broadcast → SDPA. Kept as the fallback because
// (a) it goes through the GPU plugin's micro-SDPA fast path (FlashAttention
// tiles via DPAS), and (b) the manual path above is still being validated.
auto tile_kv = [&](int64_t n_heads, int64_t n_heads_kv, int64_t hs, ov::Output<Node> kv) {
int64_t f = n_heads / n_heads_kv;
if (f > 1 && n_heads_kv > 1) {
ov::Output<ov::Node> kv_broadcast_shape;
ov::Output<ov::Node> kv_unsqueezed;
ov::Output<ov::Node> new_kv_shape;
auto unsqueeze_axes = ov::op::v0::Constant::create(ov::element::i64, Shape{}, {2});
kv_unsqueezed = std::make_shared<ov::op::v0::Unsqueeze>(kv, unsqueeze_axes);
kv_broadcast_shape = ov::op::v0::Constant::create(ov::element::i64, {5},
{(int64_t) 1, (int64_t) 1, f, (int64_t) 1, (int64_t) 1});
new_kv_shape =
ov::op::v0::Constant::create(ov::element::i64, {4}, {(int64_t) 0, n_heads, (int64_t) -1, hs});
// ov::element::i64, {5}, {(int64_t) 1, (int64_t) 1, factor, (int64_t) 1, (int64_t) 1});
//new_kv_shape =
// ov::op::v0::Constant::create(ov::element::i64, {4}, {(int64_t) 0, num_heads, (int64_t) -1, head_size});
kv = std::make_shared<ov::op::v3::Broadcast>(kv_unsqueezed, kv_broadcast_shape,
ov::op::BroadcastType::BIDIRECTIONAL);
kv = std::make_shared<ov::op::v1::Reshape>(kv, new_kv_shape, true);
}
return kv;
};
//auto q_shape = context.get_input_shape(0).to_shape();
//auto k_shape = context.get_input_shape(1).to_shape();
//k = tile_kv(q_shape[1], k_shape[1], q_shape[3], k);
//v = tile_kv(q_shape[1], k_shape[1], q_shape[3], v);
k = tile_kv(num_heads, num_heads_kv, head_size, k);
v = tile_kv(num_heads, num_heads_kv, head_size, v);
constexpr auto causal = false;
if (has_mask) {
auto sdpa = std::make_shared<ov::op::v13::ScaledDotProductAttention>(q, k, v, mask, scale_node, causal);
res = std::make_shared<ov::op::v1::Transpose>(
sdpa, ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 2, 1, 3}));
} else {
auto sdpa = std::make_shared<ov::op::v13::ScaledDotProductAttention>(q, k, v, scale_node, causal);
res = std::make_shared<ov::op::v1::Transpose>(
sdpa, ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 2, 1, 3}));
}
res = std::make_shared<ov::op::v0::Convert>(res, ov::element::f32);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,407 @@
#include "gated_delta_net.hpp"
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include "ggml-openvino/ggml-openvino-extra.h"
#include <cmath>
#include <cstdint>
#include <memory>
#include <numeric>
#include <openvino/op/add.hpp>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/exp.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/less.hpp>
#include <openvino/op/loop.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/reduce_mean.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/sqrt.hpp>
#include <openvino/op/subtract.hpp>
#include <openvino/op/tile.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
static OutputVector translate_gated_delta_net_ref(const NodeContext & context);
static bool match_gdn_l2_norm(const Output<Node> & normalized, Output<Node> & input, float & eps) {
// Match the RMSNorm decomposition emitted by translate_rms_norm, followed by GGML SCALE.
const auto scale = ov::as_type_ptr<ov::op::v1::Multiply>(normalized.get_node_shared_ptr());
if (!scale) {
return false;
}
const auto factor = ov::as_type_ptr<ov::op::v0::Constant>(scale->get_input_node_shared_ptr(1));
const auto rms = ov::as_type_ptr<ov::op::v1::Multiply>(scale->get_input_node_shared_ptr(0));
if (!factor || ov::shape_size(factor->get_shape()) != 1 || !rms) {
return false;
}
const auto x = rms->input_value(0);
const auto & shape = x.get_partial_shape();
if (x.get_element_type() != ov::element::f32 || shape.rank() != 4 || shape[3].is_dynamic() ||
shape[3].get_length() <= 0 || normalized.get_partial_shape() != shape) {
return false;
}
const float dim = static_cast<float>(shape[3].get_length());
if (factor->cast_vector<float>()[0] != 1.0f / std::sqrt(dim)) {
return false;
}
const auto reciprocal = ov::as_type_ptr<ov::op::v1::Divide>(rms->get_input_node_shared_ptr(1));
if (!reciprocal) {
return false;
}
const auto one = ov::as_type_ptr<ov::op::v0::Constant>(reciprocal->get_input_node_shared_ptr(0));
const auto root = ov::as_type_ptr<ov::op::v0::Sqrt>(reciprocal->get_input_node_shared_ptr(1));
if (!one || ov::shape_size(one->get_shape()) != 1 || one->cast_vector<float>()[0] != 1.0f || !root) {
return false;
}
const auto add = ov::as_type_ptr<ov::op::v1::Add>(root->get_input_node_shared_ptr(0));
if (!add) {
return false;
}
const auto mean = ov::as_type_ptr<ov::op::v1::ReduceMean>(add->get_input_node_shared_ptr(0));
const auto rms_eps = ov::as_type_ptr<ov::op::v0::Constant>(add->get_input_node_shared_ptr(1));
if (!mean || !mean->get_keep_dims() || !rms_eps || ov::shape_size(rms_eps->get_shape()) != 1) {
return false;
}
const auto axes = ov::as_type_ptr<ov::op::v0::Constant>(mean->get_input_node_shared_ptr(1));
const auto square = ov::as_type_ptr<ov::op::v1::Multiply>(mean->get_input_node_shared_ptr(0));
if (!axes || axes->cast_vector<int64_t>() != std::vector<int64_t>{-1} || !square ||
square->input_value(0) != x || square->input_value(1) != x) {
return false;
}
// RMSNorm(x, rms_eps) / sqrt(D) = x / sqrt(sum(x*x) + D*rms_eps).
eps = dim * rms_eps->cast_vector<float>()[0];
if (!std::isfinite(eps) || eps <= 0.0f) {
return false;
}
input = x;
return true;
}
OutputVector translate_gated_delta_net(const NodeContext & context) {
auto v_shape = context.get_input_shape(2).to_shape(); // [B, T, H_v, S_v]
auto q_shape = context.get_input_shape(0).to_shape(); // [B, T, H_k, S_k]
// Fused GatedDeltaNet op only supports scalar gate (kda=0).
// Fall back to reference implementation for per-key-dimension gating.
// if (kda) {
// return translate_gated_delta_net_ref(context);
// }
// const int64_t B = v_shape[0];
// const int64_t T = v_shape[1];
const int64_t H_v = v_shape[2];
const int64_t S_v = v_shape[3];
const int64_t H_k = q_shape[2];
// const int64_t S_k = q_shape[3];
auto q = context.get_input(0);
auto k = context.get_input(1);
auto v = process_view_input(context, 2, H_v * S_v, 3);
auto g = context.get_input(3);
auto beta = context.get_input(4);
auto state = context.get_input(5);
Output<Node> raw_q, raw_k;
float q_eps = 1e-6f, k_eps = 1e-6f;
const bool fuse_qk_l2norm = ggml_openvino_is_gpu() &&
match_gdn_l2_norm(q, raw_q, q_eps) && match_gdn_l2_norm(k, raw_k, k_eps);
if (fuse_qk_l2norm) {
// Keep head tiling below; GDN applies normalization per head and keeps its attention scale.
q = raw_q;
k = raw_k;
}
// ggml maps GQA heads in tiled order, while OV GDN maps repeated heads in grouped order.
if (H_v != H_k) {
const int64_t repeat = H_v / H_k;
auto repeats = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, repeat, 1});
q = std::make_shared<ov::op::v0::Tile>(q, repeats);
k = std::make_shared<ov::op::v0::Tile>(k, repeats);
}
if (context.get_view_input_size(2)) {
// Same as l2_norm case 1
v = std::make_shared<ov::op::v0::Squeeze>(v, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
auto v_shape = context.get_input_shape(2).to_shape();
std::vector<int64_t> reshape_pattern = {0, 0, (int64_t) v_shape[2], (int64_t) v_shape[3]};
v = std::make_shared<ov::op::v1::Reshape>(
v, ov::op::v0::Constant::create(ov::element::i64, {4}, reshape_pattern), true);
}
// ggml state layout (OV notation): [B, H_v, value_dim, key_dim]
// GatedDeltaNet op expects: [B, H_v, key_dim, value_dim]
auto state_perm = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{0, 1, 3, 2});
state = std::make_shared<ov::op::v1::Transpose>(state, state_perm);
g = std::make_shared<ov::op::v0::Squeeze>(g, ov::op::v0::Constant::create(ov::element::i64, {1}, {3}));
beta = std::make_shared<ov::op::v0::Squeeze>(beta, ov::op::v0::Constant::create(ov::element::i64, {1}, {3}));
if (context.has_input("chunk_valid_len")) {
// The last prefill chunk is padded with fabricated tokens. The recurrence is
// S_t = S_{t-1} * exp(g_t) + k_t (x) ((v_t - S_{t-1}^T k_t) * beta_t)
// so forcing g = 0 and beta = 0 makes a padded step an exact identity and keeps the final
// state equal to the state after the last real token. Attention output at those positions
// is garbage but never read.
const auto & g_ps = g.get_partial_shape();
FRONT_END_OP_CONVERSION_CHECK(g_ps.rank().is_static() && g_ps.rank().get_length() == 3 && g_ps[1].is_static(),
"GATED_DELTA_NET pad masking requires a static token dimension");
const int64_t n_tokens = g_ps[1].get_length();
std::vector<int64_t> positions(n_tokens);
std::iota(positions.begin(), positions.end(), 0);
auto valid = std::make_shared<ov::op::v1::Less>(
ov::op::v0::Constant::create(ov::element::i64, {(size_t) n_tokens}, positions),
context.get_input("chunk_valid_len"));
auto mask = std::make_shared<ov::op::v0::Unsqueeze>(
std::make_shared<ov::op::v0::Convert>(valid, g.get_element_type()),
ov::op::v0::Constant::create(ov::element::i64, {2}, std::vector<int64_t>{0, 2}));
g = std::make_shared<ov::op::v1::Multiply>(g, mask);
beta = std::make_shared<ov::op::v1::Multiply>(beta, mask);
}
// std::cout << "GatedDeltaNet input shapes: q=" << q.get_partial_shape() << ", k=" << k.get_partial_shape()
// << ", v=" << v.get_partial_shape() << ", g=" << g.get_partial_shape()
// << ", beta=" << beta.get_partial_shape() << ", state=" << state.get_partial_shape() << std::endl;
auto gdn = std::make_shared<ov::op::internal::GatedDeltaNet>(q, k, v, state, g, beta, fuse_qk_l2norm, q_eps, k_eps);
auto attn_4d = gdn->output(0);
auto state_4d = gdn->output(1); // [B, H_v, key_dim, value_dim]
// std::cout << "GatedDeltaNet output shapes: attn=" << gdn->output(0).get_partial_shape()
// << ", new_state=" << gdn->output(1).get_partial_shape() << std::endl;
// Transpose output state back to ggml layout [B, H_v, value_dim, key_dim]
auto state_transposed = std::make_shared<ov::op::v1::Transpose>(state_4d, state_perm);
if (context.get_output_names().size() == 2) {
// The canonical graph consumes the packed GGML result only through separate attention
// and state VIEWs. Publish the native outputs under those VIEW names to avoid
// flatten -> concat -> reshape -> slice -> reshape chains. This also works for B > 1.
return rename_outputs_with_suffix({attn_4d, state_transposed}, context.get_name());
}
auto flat_shape_1d = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto attn = std::make_shared<ov::op::v1::Reshape>(attn_4d, flat_shape_1d, false);
auto new_state = std::make_shared<ov::op::v1::Reshape>(state_transposed, flat_shape_1d, false);
auto packed = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{attn, new_state}, 0);
auto out_shape = ov::op::v0::Constant::create(ov::element::i64, {4},
std::vector<int64_t>{1, 1, -1 /*T * B + S_v * B*/, S_v * H_v});
auto res = std::make_shared<ov::op::v1::Reshape>(packed, out_shape, false);
return rename_outputs_with_suffix({res}, context.get_name());
}
static OutputVector translate_gated_delta_net_ref(const NodeContext & context) {
num_inputs_check(context, 6, 6);
// Inputs (OV shapes are reversed from ggml):
// ggml: q[S_k, H_k, T, B], k[S_k, H_k, T, B], v[S_v, H_v, T, B]
// OV: q[B, T, H_k, S_k], k[B, T, H_k, S_k], v[B, T, H_v, S_v]
// ggml: g[1 or S_v, H_v, T, B], beta[1, H_v, T, B]
// OV: g[B, T, H_v, 1 or S_v], beta[B, T, H_v, 1]
// ggml: state[S_v, S_v, H_v, B]
// OV: state[B, H_v, S_v, S_v]
auto q = process_view_input_new(context, 0);
auto k = process_view_input_new(context, 1);
auto v = process_view_input_new(context, 2);
auto g = process_view_input_new(context, 3);
auto beta = process_view_input_new(context, 4);
auto state = process_view_input_new(context, 5);
auto v_shape = context.get_input_shape(2).to_shape(); // [B, T, H_v, S_v]
auto q_shape = context.get_input_shape(0).to_shape(); // [B, T, H_k, S_k]
auto g_shape = context.get_input_shape(3).to_shape(); // [B, T, H_v, 1 or S_v]
const int64_t B = v_shape[0];
const int64_t T = v_shape[1];
const int64_t H_v = v_shape[2];
const int64_t S_v = v_shape[3];
const int64_t H_k = q_shape[2];
const bool kda = (g_shape[3] == (size_t) S_v);
const int64_t rq1 = H_v / H_k; // head repeat factor
const float scale = 1.0f / std::sqrt((float) S_v);
auto axis_1 = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto axis_2 = ov::op::v0::Constant::create(ov::element::i64, {1}, {2});
// Transpose inputs from [B, T, H, S] to [B, H, T, S] for easier per-head processing
auto perm_0213 = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{0, 2, 1, 3});
auto q_t = std::make_shared<ov::op::v1::Transpose>(q, perm_0213); // [B, H_k, T, S_k]
auto k_t = std::make_shared<ov::op::v1::Transpose>(k, perm_0213); // [B, H_k, T, S_k]
auto v_t = std::make_shared<ov::op::v1::Transpose>(v, perm_0213); // [B, H_v, T, S_v]
auto g_t = std::make_shared<ov::op::v1::Transpose>(g, perm_0213); // [B, H_v, T, 1 or S_v]
auto beta_t = std::make_shared<ov::op::v1::Transpose>(beta, perm_0213); // [B, H_v, T, 1]
// Broadcast Q, K heads to match V heads if GQA is used (H_v > H_k)
ov::Output<ov::Node> q_bh = q_t;
ov::Output<ov::Node> k_bh = k_t;
if (rq1 > 1) {
auto q_unsq = std::make_shared<ov::op::v0::Unsqueeze>(q_t, axis_2); // [B, H_k, 1, T, S]
auto k_unsq = std::make_shared<ov::op::v0::Unsqueeze>(k_t, axis_2); // [B, H_k, 1, T, S]
auto bcast_shape = ov::op::v0::Constant::create(ov::element::i64, {5}, std::vector<int64_t>{1, 1, rq1, 1, 1});
auto q_bcast =
std::make_shared<ov::op::v3::Broadcast>(q_unsq, bcast_shape, ov::op::BroadcastType::BIDIRECTIONAL);
auto k_bcast =
std::make_shared<ov::op::v3::Broadcast>(k_unsq, bcast_shape, ov::op::BroadcastType::BIDIRECTIONAL);
// Transpose [B, H_k, rq1, T, S] -> [B, rq1, H_k, T, S] so that reshape merges
// as [rq1, H_k] giving repeat-blocks pattern matching CPU: iq1 = iv1 % H_k
auto perm_5d = ov::op::v0::Constant::create(ov::element::i64, {5}, std::vector<int64_t>{0, 2, 1, 3, 4});
auto q_transposed = std::make_shared<ov::op::v1::Transpose>(q_bcast, perm_5d);
auto k_transposed = std::make_shared<ov::op::v1::Transpose>(k_bcast, perm_5d);
auto new_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{B, H_v, T, S_v});
q_bh = std::make_shared<ov::op::v1::Reshape>(q_transposed, new_shape, false);
k_bh = std::make_shared<ov::op::v1::Reshape>(k_transposed, new_shape, false);
}
// Merge batch and head dims: [B*H_v, T, S_v]
auto merge_bh = [&](const ov::Output<ov::Node> & x, int64_t last_dim) {
auto shape = ov::op::v0::Constant::create(ov::element::i64, {3}, std::vector<int64_t>{B * H_v, T, last_dim});
return std::make_shared<ov::op::v1::Reshape>(x, shape, false);
};
auto q_m = merge_bh(q_bh, S_v); // [B*H_v, T, S_v]
auto k_m = merge_bh(k_bh, S_v); // [B*H_v, T, S_v]
auto v_m = merge_bh(v_t, S_v); // [B*H_v, T, S_v]
auto g_m = merge_bh(g_t, kda ? S_v : 1); // [B*H_v, T, 1 or S_v]
auto beta_m = merge_bh(beta_t, 1); // [B*H_v, T, 1]
// State: [B, H_v, S_v, S_v] -> [B*H_v, S_v, S_v]
auto state_shape = ov::op::v0::Constant::create(ov::element::i64, {3}, std::vector<int64_t>{B * H_v, S_v, S_v});
auto state_m = std::make_shared<ov::op::v1::Reshape>(state, state_shape, false);
auto scale_const = ov::op::v0::Constant::create(ov::element::f32, {}, std::vector<float>{scale});
// --- Build Loop body ---
// Body parameters (no iteration counter needed, use -1 in special ports)
auto body_state = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape::dynamic());
auto body_q = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape::dynamic());
auto body_k = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape::dynamic());
auto body_v = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape::dynamic());
auto body_g = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape::dynamic());
auto body_beta = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape::dynamic());
auto body_iter = std::make_shared<ov::op::v0::Parameter>(ov::element::i64, ov::Shape{1});
// Condition output (always true - we rely on trip_count for termination)
auto body_cond_out = ov::op::v0::Constant::create(ov::element::boolean, ov::Shape{1}, std::vector<bool>{true});
// Gather current token from invariant inputs using iteration counter
auto q_t_cur = std::make_shared<ov::op::v8::Gather>(body_q, body_iter, axis_1); // [B*H_v, 1, S_v]
auto k_t_cur = std::make_shared<ov::op::v8::Gather>(body_k, body_iter, axis_1); // [B*H_v, 1, S_v]
auto v_t_cur = std::make_shared<ov::op::v8::Gather>(body_v, body_iter, axis_1); // [B*H_v, 1, S_v]
auto g_t_cur = std::make_shared<ov::op::v8::Gather>(body_g, body_iter, axis_1); // [B*H_v, 1, 1 or S_v]
auto b_t_cur = std::make_shared<ov::op::v8::Gather>(body_beta, body_iter, axis_1); // [B*H_v, 1, 1]
// Squeeze token dim
auto q_cur = std::make_shared<ov::op::v0::Squeeze>(q_t_cur, axis_1); // [B*H_v, S_v]
auto k_cur = std::make_shared<ov::op::v0::Squeeze>(k_t_cur, axis_1); // [B*H_v, S_v]
auto v_cur = std::make_shared<ov::op::v0::Squeeze>(v_t_cur, axis_1); // [B*H_v, S_v]
auto g_cur = std::make_shared<ov::op::v0::Squeeze>(g_t_cur, axis_1); // [B*H_v, 1 or S_v]
auto b_cur = std::make_shared<ov::op::v0::Squeeze>(b_t_cur, axis_1); // [B*H_v, 1]
// Step 1: Apply decay gate to state
auto exp_g = std::make_shared<ov::op::v0::Exp>(g_cur); // [B*H_v, 1 or S_v]
auto exp_g_unsq = std::make_shared<ov::op::v0::Unsqueeze>(exp_g, axis_1); // [B*H_v, 1, 1 or S_v]
auto state_decayed = std::make_shared<ov::op::v1::Multiply>(body_state, exp_g_unsq); // [B*H_v, S_v, S_v]
// Step 2: delta = (v - S @ k) * beta
auto k_col = std::make_shared<ov::op::v0::Unsqueeze>(k_cur, axis_2); // [B*H_v, S_v, 1]
auto sk = std::make_shared<ov::op::v0::MatMul>(state_decayed, k_col, false, false); // [B*H_v, S_v, 1]
auto sk_sq = std::make_shared<ov::op::v0::Squeeze>(sk, axis_2); // [B*H_v, S_v]
auto v_minus_sk = std::make_shared<ov::op::v1::Subtract>(v_cur, sk_sq); // [B*H_v, S_v]
auto delta = std::make_shared<ov::op::v1::Multiply>(v_minus_sk, b_cur); // [B*H_v, S_v]
// Step 3: state += outer(delta, k)
auto delta_col = std::make_shared<ov::op::v0::Unsqueeze>(delta, axis_2); // [B*H_v, S_v, 1]
auto k_row = std::make_shared<ov::op::v0::Unsqueeze>(k_cur, axis_1); // [B*H_v, 1, S_v]
auto outer_prod = std::make_shared<ov::op::v0::MatMul>(delta_col, k_row, false, false); // [B*H_v, S_v, S_v]
auto state_updated = std::make_shared<ov::op::v1::Add>(state_decayed, outer_prod); // [B*H_v, S_v, S_v]
// Step 4: attn_out = S @ q * scale
auto q_col = std::make_shared<ov::op::v0::Unsqueeze>(q_cur, axis_2); // [B*H_v, S_v, 1]
auto sq = std::make_shared<ov::op::v0::MatMul>(state_updated, q_col, false, false); // [B*H_v, S_v, 1]
auto sq_squeezed = std::make_shared<ov::op::v0::Squeeze>(sq, axis_2); // [B*H_v, S_v]
auto attn_out = std::make_shared<ov::op::v1::Multiply>(sq_squeezed, scale_const); // [B*H_v, S_v]
// Unsqueeze attn_out to [B*H_v, 1, S_v] for scan output concatenation
auto attn_out_unsq = std::make_shared<ov::op::v0::Unsqueeze>(attn_out, axis_1); // [B*H_v, 1, S_v]
// --- Assemble Loop ---
// Body: results = [condition, state_updated, attn_out_unsq]
auto body = std::make_shared<ov::Model>(
ov::OutputVector{body_cond_out, state_updated, attn_out_unsq},
ov::ParameterVector{body_iter, body_state, body_q, body_k, body_v, body_g, body_beta});
auto trip_count = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector<int64_t>{T});
auto exec_cond = ov::op::v0::Constant::create(ov::element::boolean, ov::Shape{1}, std::vector<bool>{true});
auto loop = std::make_shared<ov::op::v5::Loop>(trip_count, exec_cond);
loop->set_function(body);
loop->set_special_body_ports(ov::op::v5::Loop::SpecialBodyPorts{0, 0});
// Carried state: feeds back from body output 1 to body_state param
loop->set_merged_input(body_state, state_m, state_updated);
// Invariant inputs: passed through unchanged each iteration
loop->set_invariant_input(body_q, q_m);
loop->set_invariant_input(body_k, k_m);
loop->set_invariant_input(body_v, v_m);
loop->set_invariant_input(body_g, g_m);
loop->set_invariant_input(body_beta, beta_m);
// Loop outputs:
// 1) Final state (last iteration value of state_updated)
auto final_state_out = loop->get_iter_value(state_updated, -1); // [B*H_v, S_v, S_v]
// 2) Concatenated attention outputs across all iterations along axis 1
auto attn_concat_out = loop->get_concatenated_slices(attn_out_unsq, 0, 1, 1, -1, 1); // [B*H_v, T, S_v]
// --- Pack outputs to match ggml layout ---
// ggml output ne = {S_v*H, T*B + S_v*B, 1, 1} -> OV [1, 1, T*B+S_v*B, S_v*H_v]
// attn: [B, T, H_v, S_v] row-major, state: [B, H_v, S_v, S_v] row-major
// attn: [B*H_v, T, S_v] -> [B, H_v, T, S_v] -> transpose to [B, T, H_v, S_v] -> flatten
auto attn_4d_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{B, H_v, T, S_v});
auto attn_4d = std::make_shared<ov::op::v1::Reshape>(attn_concat_out, attn_4d_shape, false);
auto attn_perm = std::make_shared<ov::op::v1::Transpose>(attn_4d, perm_0213); // [B, T, H_v, S_v]
auto flat_shape_1d = ov::op::v0::Constant::create(ov::element::i64, {1}, std::vector<int64_t>{-1});
auto attn_1d = std::make_shared<ov::op::v1::Reshape>(attn_perm, flat_shape_1d, false);
// state: [B*H_v, S_v, S_v] -> [B, H_v, S_v, S_v] -> flatten
auto state_4d_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{B, H_v, S_v, S_v});
auto state_4d = std::make_shared<ov::op::v1::Reshape>(final_state_out, state_4d_shape, false);
if (context.get_output_names().size() == 2) {
// Match the fused translator's direct attention/state contract.
return rename_outputs_with_suffix({attn_perm, state_4d}, context.get_name());
}
auto state_1d = std::make_shared<ov::op::v1::Reshape>(state_4d, flat_shape_1d, false);
// Concat [attn | state] and reshape to final output
auto packed = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{attn_1d, state_1d}, 0);
auto out_shape =
ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, T * B + S_v * B, S_v * H_v});
auto res = std::make_shared<ov::op::v1::Reshape>(packed, out_shape, false);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,65 @@
#pragma once
#include "openvino/op/op.hpp"
namespace ov::op::internal {
/// \note GatedDeltaNet op class is under development and subject to change
///
/// \brief Operator performing Gated Delta Net computation
/// \ingroup ov_ops_cpp_api
class OPENVINO_API GatedDeltaNet : public ov::op::Op {
public:
OPENVINO_OP("GatedDeltaNet")
GatedDeltaNet() = default;
/// \brief Constructs a GatedDeltaNet operation.
///
/// \param query Query tensor input.
/// \param key Key tensor input.
/// \param value Value tensor input.
/// \param recurrent_state Initial recurrent state tensor.
/// \param gate Gate tensor controlling state decay/update.
/// \param beta Beta tensor scaling the delta update.
/// \param fuse_qk_l2norm Enables fusing q/k L2-normalization into this op.
/// \param q_l2_norm_eps Epsilon used for query L2-normalization when fusion is enabled.
/// \param k_l2_norm_eps Epsilon used for key L2-normalization when fusion is enabled.
GatedDeltaNet(const Output<Node>& query,
const Output<Node>& key,
const Output<Node>& value,
const Output<Node>& recurrent_state,
const Output<Node>& gate,
const Output<Node>& beta,
const bool fuse_qk_l2norm = false,
const float q_l2_norm_eps = 1e-6F,
const float k_l2_norm_eps = 1e-6F);
/// \brief Constructs a GatedDeltaNet operation from input vector.
///
/// \param args Input tensor vector in order: query, key, value, recurrent_state, gate, beta.
/// \param fuse_qk_l2norm Enables fusing q/k L2-normalization into this op.
/// \param q_l2_norm_eps Epsilon used for query L2-normalization when fusion is enabled.
/// \param k_l2_norm_eps Epsilon used for key L2-normalization when fusion is enabled.
GatedDeltaNet(const ov::OutputVector& args,
const bool fuse_qk_l2norm = false,
const float q_l2_norm_eps = 1e-6F,
const float k_l2_norm_eps = 1e-6F);
void validate_and_infer_types() override;
bool visit_attributes(AttributeVisitor& visitor) override;
std::shared_ptr<ov::Node> clone_with_new_inputs(const ov::OutputVector& new_args) const override;
bool get_fuse_qk_l2norm() const {
return m_fuse_qk_l2norm;
}
float get_q_l2_norm_eps() const {
return m_q_l2_norm_eps;
}
float get_k_l2_norm_eps() const {
return m_k_l2_norm_eps;
}
private:
bool m_fuse_qk_l2norm = false;
float m_q_l2_norm_eps = 1e-6F;
float m_k_l2_norm_eps = 1e-6F;
};
} // namespace ov::op::internal
@@ -0,0 +1,43 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
// Local mirror of OpenVINO's internal ov::op::internal::GatherMatmul op.
//
// The op class body (validate_and_infer_types / clone_with_new_inputs) is
// provided by the linked libopenvino.so; only the declaration is needed here so
// the backend can construct the node directly (same approach as GatedDeltaNet).
// The class layout must stay in sync with
// openvino/src/common/transformations/include/ov_ops/gather_matmul.hpp
//
// \note GatherMatmul op class is under development and subject to change.
#pragma once
#include "openvino/op/op.hpp"
namespace ov::op::internal {
class OPENVINO_API GatherMatmul : public ov::op::Op {
public:
OPENVINO_OP("GatherMatmul")
GatherMatmul() = default;
GatherMatmul(const ov::Output<Node>& A,
const ov::Output<Node>& B,
const ov::Output<Node>& indices,
const ov::Output<Node>& bias);
GatherMatmul(const ov::Output<Node>& A, const ov::Output<Node>& B, const ov::Output<Node>& indices);
std::shared_ptr<Node> clone_with_new_inputs(const ov::OutputVector& new_args) const override;
void validate_and_infer_types() override;
private:
// the weights matrix B is expected to have the transposed form [group, N, K]
static constexpr bool transp_a = false;
static constexpr bool transp_b = true;
};
} // namespace ov::op::internal
@@ -0,0 +1,178 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <climits>
#include <openvino/core/node.hpp>
#include <openvino/core/node_output.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/unsqueeze.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_get_rows(const NodeContext & context) {
num_inputs_check(context, 2, 2);
Output<Node> res;
Output<Node> data;
int64_t row_offset = 0;
if (context.get_view_input_size(0) > 0 && context.get_input(0).get_partial_shape().rank() == 2) {
// A row range view over a 2D weight Constant folds into the gather indices,
// which keeps the dequantization subgraph intact for the plugins.
const auto view_shape = context.get_view_input_ggml_shape(0, 0);
const auto view_stride = context.get_view_input_stride(0, 0);
const size_t view_offset = context.get_view_input_offset(0, 0);
const size_t row_bytes = view_stride[2];
data = context.get_input(0);
FRONT_END_OP_CONVERSION_CHECK(row_bytes > 0 && view_offset % row_bytes == 0 &&
data.get_partial_shape()[1].compatible(view_shape[3]),
"GET_ROWS: view over a weight must be a row range");
row_offset = static_cast<int64_t>(view_offset / row_bytes);
} else {
data = process_view_input_new(context, 0);
}
auto op_case = context.get_op_case();
if (op_case == 3 || op_case == 4) {
return {data};
}
ov::Output<ov::Node> indices;
if ((op_case == 1 || op_case == 2) && context.has_input("s_copy_active_slot_len")) {
// Recurrent state reorder (inp->s_copy): slice the active (op_case 1) or extra (op_case 2)
// segment from the s_copy index list at runtime, instead of baking the static view offset,
// so the cached IR works for any number of active sequences.
auto s_copy = context.get_input(1);
auto len = context.get_input("s_copy_active_slot_len");
auto step = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
if (op_case == 1) {
auto begin = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
indices = std::make_shared<ov::op::v8::Slice>(s_copy, begin, len, step, axis);
} else {
auto end = ov::op::v0::Constant::create(ov::element::i64, {1}, {INT_MAX});
indices = std::make_shared<ov::op::v8::Slice>(s_copy, len, end, step, axis);
}
} else {
indices = process_view_input_new(context, 1);
}
// data[1,b,x,y] ind[1,1,b,x'] test-backend-ops case
// data[x,y] ind[1,1,1,x'] normal case
// Squeeze the leading dims down to [b,x']. Stateful models drop one rank, so a hardcoded
// {0,1} would also strip the batch dim whenever b == 1 (every decode step).
const auto indices_rank = indices.get_partial_shape().rank();
FRONT_END_OP_CONVERSION_CHECK(indices_rank.is_static(), "Expected static rank for GET_ROWS indices");
std::vector<int64_t> indices_squeeze_axes;
for (int64_t i = 0; i + 2 < indices_rank.get_length(); ++i) {
indices_squeeze_axes.push_back(i);
}
if (!indices_squeeze_axes.empty()) {
indices = std::make_shared<ov::op::v0::Squeeze>(
indices, ov::op::v0::Constant::create(ov::element::i64, {indices_squeeze_axes.size()},
indices_squeeze_axes));
}
if (row_offset != 0) {
indices = std::make_shared<ov::op::v1::Add>(
indices, ov::op::v0::Constant::create(indices.get_element_type(), {}, {row_offset}));
}
if (data.get_partial_shape().rank() == 4) {
if (!(data.get_partial_shape()[1].is_dynamic()) && data.get_partial_shape()[1].get_length() == 1) {
// Work-around for a bug in ov cpu plugin for test-backend-ops
data = std::make_shared<ov::op::v0::Squeeze>(data,
ov::op::v0::Constant::create(ov::element::i64, {2}, {0, 1}));
auto axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {0});
res = std::make_shared<ov::op::v8::Gather>(data, indices, axis);
} else {
auto axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {1});
data =
std::make_shared<ov::op::v0::Squeeze>(data, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
// data: [batch, rows, ...], indices: [batch, n] - this is a batched gather
// (batch_dims=1) along the rows axis. The data and indices batch dims are
// logically equal (both == n_tokens) but reach this node through independent
// reshapes, so the GPU plugin's gather shape inference cannot prove
// data.shape[0] == indices.shape[0] and rejects the node. We must tie both
// batch dims to the SAME value, and crucially that value must stay DYNAMIC.
const auto data_ps = data.get_partial_shape();
const auto idx_ps = indices.get_partial_shape();
const bool data_batch_static = data_ps.rank().is_static() && data_ps[0].is_static();
const bool idx_batch_dynamic = idx_ps.rank().is_dynamic() || idx_ps[0].is_dynamic();
if (data_batch_static && idx_batch_dynamic) {
// MoE per-expert-scale path: `data` is a statically-tiled REPEAT
// (ggml_repeat_4d(scale, 1, n_expert, n_tokens, 1)) whose batch dim is a
// compile-time-constant n_tokens, and every batch slice is IDENTICAL (it was
// tiled from a single [1, n_expert, 1] scale). `indices` (selected_experts)
// carries the genuinely dynamic token dim. Broadcasting indices up to the
// static data batch (the naive fix) would freeze the token dim to the
// captured prefill length, and that static value then flows through the
// gather into the residual stream, making every following decoder layer
// static -> triggers the GPU in-place-concat KV-cache corruption (only
// layer 0 stays dynamic). A static->dynamic Broadcast cannot expand, so
// instead collapse the redundant data batch to 1 and broadcast 1->dynamic to
// match the indices batch. Mathematically identical (the slices are equal),
// and the whole graph stays dynamic.
auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto axis0 = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto data_b1 = std::make_shared<ov::op::v8::Slice>(data, zero, one, one, axis0); // [1, rows, ...]
auto idx_shape = std::make_shared<ov::op::v3::ShapeOf>(indices, ov::element::i64);
auto idx_batch = get_dimensions(idx_shape, {0}); // [batch] (dynamic)
auto data_b1_shape = std::make_shared<ov::op::v3::ShapeOf>(data_b1, ov::element::i64);
const auto rank = data_ps.rank().get_length();
std::vector<int> rest_axes;
for (int a = 1; a < rank; ++a) {
rest_axes.push_back(a);
}
auto data_rest = get_dimensions(data_b1_shape, rest_axes); // [rows, ...]
auto data_target = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{idx_batch, data_rest}, 0);
data =
std::make_shared<ov::op::v3::Broadcast>(data_b1, data_target, ov::op::BroadcastType::BIDIRECTIONAL);
res = std::make_shared<ov::op::v8::Gather>(data, indices, axis, 1);
} else {
// General case: tie the indices batch to the data batch (the data batch is
// already dynamic, e.g. the routing-weights gather whose data comes from the
// activations). Broadcast indices to [data_batch, indices_n].
auto data_shape = std::make_shared<ov::op::v3::ShapeOf>(data, ov::element::i64);
auto data_batch = get_dimensions(data_shape, {0}); // [batch]
auto idx_shape = std::make_shared<ov::op::v3::ShapeOf>(indices, ov::element::i64);
auto idx_n = get_dimensions(idx_shape, {1}); // [n]
auto idx_target = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{data_batch, idx_n}, 0);
indices = std::make_shared<ov::op::v3::Broadcast>(indices, idx_target,
ov::op::BroadcastType::BIDIRECTIONAL);
res = std::make_shared<ov::op::v8::Gather>(data, indices, axis, 1);
}
}
} else if (context.is_stateful() && data.get_partial_shape().rank() == 3) {
auto axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {1});
res = std::make_shared<ov::op::v8::Gather>(data, indices, axis, 1);
} else {
auto axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {0});
res = std::make_shared<ov::op::v8::Gather>(data, indices, axis);
}
if (res.get_element_type() != context.get_output_type()) {
res = std::make_shared<ov::op::v0::Convert>(res, context.get_output_type());
}
if (!(context.is_stateful())) {
res = std::make_shared<ov::op::v0::Unsqueeze>(res, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,75 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/core/node_output.hpp>
#include <openvino/op/clamp.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/gelu.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/sigmoid.hpp>
#include <openvino/op/slice.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_glu_geglu(const NodeContext & context) {
num_inputs_check(context, 1, 2);
ov::Output<ov::Node> src0;
ov::Output<ov::Node> src1;
if (context.get_input_size() == 2) {
// Inputs may be VIEW slices of a combined gate_up tensor (MoE experts):
// resolve them so each half has its real sliced shape, not the base tensor.
src0 = process_view_input_new(context, 0);
src1 = process_view_input_new(context, 1);
} else {
// GGML splits along ne[0] (OV last axis) using floor division: nc = ne[0] / 2.
// Both halves are nc elements; if the dimension is odd, the last element is dropped.
// Use Slice instead of Split to handle odd dimensions correctly.
// Resolve a VIEW input (e.g. non-contiguous slice) to its real shape first.
auto combined = process_view_input_new(context, 0);
auto combined_shape = combined.get_partial_shape();
int64_t last_dim_val = combined_shape[combined_shape.rank().get_length() - 1].get_length();
int64_t nc = last_dim_val / 2;
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto step = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto start0 = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto stop0 = ov::op::v0::Constant::create(ov::element::i64, {1}, {nc});
auto start1 = ov::op::v0::Constant::create(ov::element::i64, {1}, {nc});
auto stop1 = ov::op::v0::Constant::create(ov::element::i64, {1}, {2 * nc});
src0 = std::make_shared<ov::op::v8::Slice>(combined, start0, stop0, step, axis);
src1 = std::make_shared<ov::op::v8::Slice>(combined, start1, stop1, step, axis);
}
int32_t * params = context.get_output_op_params();
const int32_t swapped = params[1];
if (swapped) {
std::swap(src0, src1);
}
if (context.is_static()) {
// TODO: Temporary solution for NPU accuracy issue due to fp16 overflow
// To be removed once permanent solution is implemented
// Justification:
// For |x| > 5, GELU(x) ≈ max(x, 0) (behaves like ReLU)
// So Clamp(-10, 10) only affects values where GELU would return ≈ x anyway.
// The only loss: values > 10 get mapped to 10 instead of x.
// In practice, FFN intermediates rarely exceed 10 after GEGLU gating.
src0 = std::make_shared<ov::op::v0::Clamp>(src0, -10.0, 10.0);
}
auto gelu = std::make_shared<ov::op::v7::Gelu>(src0);
auto res = std::make_shared<ov::op::v1::Multiply>(gelu, src1);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,62 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/core/node_output.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/swish.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_glu_geglu_quick(const NodeContext & context) {
num_inputs_check(context, 1, 2);
ov::Output<ov::Node> src0;
ov::Output<ov::Node> src1;
if (context.get_input_size() == 2) {
src0 = process_view_input_new(context, 0);
src1 = process_view_input_new(context, 1);
} else {
// split along last axis, nc = ne[0] / 2
auto combined = process_view_input_new(context, 0);
auto combined_shape = combined.get_partial_shape();
int64_t last_dim_val = combined_shape[combined_shape.rank().get_length() - 1].get_length();
int64_t nc = last_dim_val / 2;
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto step = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto start0 = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto stop0 = ov::op::v0::Constant::create(ov::element::i64, {1}, {nc});
auto start1 = ov::op::v0::Constant::create(ov::element::i64, {1}, {nc});
auto stop1 = ov::op::v0::Constant::create(ov::element::i64, {1}, {2 * nc});
src0 = std::make_shared<ov::op::v8::Slice>(combined, start0, stop0, step, axis);
src1 = std::make_shared<ov::op::v8::Slice>(combined, start1, stop1, step, axis);
}
int32_t * params = context.get_output_op_params();
const int32_t swapped = params[1];
if (swapped) {
std::swap(src0, src1);
}
// GELU_QUICK(x) = x * sigmoid(1.702 * x)
// Create the constant in the same type as src0 to avoid f16/f32 mismatch.
auto input_type = src0.get_element_type();
auto coef = ov::op::v0::Constant::create(input_type, ov::Shape{}, {1.702f});
auto gated = std::make_shared<ov::op::v4::Swish>(src0, coef);
auto res = std::make_shared<ov::op::v1::Multiply>(gated, src1);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,131 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <cstdint>
#include <limits>
#include <memory>
#include <openvino/core/node_output.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/clamp.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/swish.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
static std::pair<ov::Output<ov::Node>, ov::Output<ov::Node>> get_glu_inputs(const NodeContext & context) {
num_inputs_check(context, 1, 2);
ov::Output<ov::Node> src0;
ov::Output<ov::Node> src1;
if (context.get_input_size() == 2) {
// Inputs may be VIEW slices of a combined gate_up tensor (MoE experts):
// resolve them so each half has its real sliced shape, not the base tensor.
src0 = process_view_input_new(context, 0);
src1 = process_view_input_new(context, 1);
} else {
// GGML splits along ne[0] (OV last axis) using floor division: nc = ne[0] / 2.
// Both halves are nc elements; if the dimension is odd, the last element is dropped.
// Use Slice instead of Split to handle odd dimensions correctly.
// Resolve a VIEW input (e.g. non-contiguous slice) to its real shape first.
auto combined = process_view_input_new(context, 0);
auto combined_shape = combined.get_partial_shape();
int64_t last_dim_val = combined_shape[combined_shape.rank().get_length() - 1].get_length();
int64_t nc = last_dim_val / 2;
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto step = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto start0 = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto stop0 = ov::op::v0::Constant::create(ov::element::i64, {1}, {nc});
auto start1 = ov::op::v0::Constant::create(ov::element::i64, {1}, {nc});
auto stop1 = ov::op::v0::Constant::create(ov::element::i64, {1}, {2 * nc});
src0 = std::make_shared<ov::op::v8::Slice>(combined, start0, stop0, step, axis);
src1 = std::make_shared<ov::op::v8::Slice>(combined, start1, stop1, step, axis);
}
int32_t * params = context.get_output_op_params();
const int32_t swapped = params[1];
if (swapped) {
std::swap(src0, src1);
}
return {src0, src1};
}
OutputVector translate_glu_swiglu(const NodeContext & context) {
auto [src0, src1] = get_glu_inputs(context);
auto silu = std::make_shared<ov::op::v4::Swish>(src0);
auto res = std::make_shared<ov::op::v1::Multiply>(silu, src1);
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_glu_swiglu_oai(const NodeContext & context) {
auto [src0, src1] = get_glu_inputs(context);
const int32_t * params = context.get_output_op_params();
const float alpha = reinterpret_cast<const float *>(params)[2];
const float limit = reinterpret_cast<const float *>(params)[3];
// Compute in f32: alpha_const/one are f32, and Swish/Add need matching input types.
auto output_type = context.get_output_type();
if (src0.get_element_type() != ov::element::f32) {
src0 = std::make_shared<ov::op::v0::Convert>(src0, ov::element::f32);
}
if (src1.get_element_type() != ov::element::f32) {
src1 = std::make_shared<ov::op::v0::Convert>(src1, ov::element::f32);
}
auto gate = std::make_shared<ov::op::v0::Clamp>(src0, -std::numeric_limits<float>::infinity(), limit);
auto alpha_const = ov::op::v0::Constant::create(ov::element::f32, {}, {alpha});
auto out_glu = std::make_shared<ov::op::v4::Swish>(gate, alpha_const);
auto up = std::make_shared<ov::op::v0::Clamp>(src1, -limit, limit);
auto one = ov::op::v0::Constant::create(ov::element::f32, {}, {1.0f});
auto up_plus_one = std::make_shared<ov::op::v1::Add>(up, one);
ov::Output<ov::Node> res = std::make_shared<ov::op::v1::Multiply>(out_glu, up_plus_one);
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_glu_swiglu_clamp(const NodeContext & context) {
auto [src0, src1] = get_glu_inputs(context);
const int32_t * params = context.get_output_op_params();
const float limit = reinterpret_cast<const float *>(params)[3];
// Compute in f32: f16 Swish/Clamp rounding drifts past the 1e-7 test tolerance.
auto output_type = context.get_output_type();
if (src0.get_element_type() != ov::element::f32) {
src0 = std::make_shared<ov::op::v0::Convert>(src0, ov::element::f32);
}
if (src1.get_element_type() != ov::element::f32) {
src1 = std::make_shared<ov::op::v0::Convert>(src1, ov::element::f32);
}
auto gate = std::make_shared<ov::op::v0::Clamp>(src0, -std::numeric_limits<float>::infinity(), limit);
auto silu = std::make_shared<ov::op::v4::Swish>(gate);
auto up = std::make_shared<ov::op::v0::Clamp>(src1, -limit, limit);
ov::Output<ov::Node> res = std::make_shared<ov::op::v1::Multiply>(silu, up);
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,239 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <cstddef>
#include <memory>
#include <openvino/core/shape.hpp>
#include <openvino/core/strides.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/extractimagepatches.hpp>
#include <openvino/op/pad.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/util/attr_types.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_im2col(const NodeContext & context) {
num_inputs_check(context, 2, 2);
const int32_t * params = context.get_output_op_params();
int32_t s0 = params[0];
int32_t s1 = params[1];
int32_t p0 = params[2];
int32_t p1 = params[3];
int32_t d0 = params[4];
int32_t d1 = params[5];
bool is_2D = params[6] == 1;
ov::Output<Node> res;
ov::Output<Node> image = context.get_input(1);
const ov::Shape kernel_shape = context.get_input(0).get_shape();
const size_t IC = is_2D ? kernel_shape[1] : kernel_shape[2];
const size_t KH = is_2D ? kernel_shape[2] : 1;
const size_t KW = kernel_shape[3];
int32_t stride_w = s0;
int32_t stride_h = is_2D ? s1 : 1;
int32_t pad_w = p0;
int32_t pad_h = is_2D ? p1 : 0;
int32_t dil_w = d0;
int32_t dil_h = is_2D ? d1 : 1;
if (!is_2D) {
// GGML input shape: [IW, IC, N, 1]
// OpenVINO input shape: [1, N, IC, IW]
// Reshape image to: [N, IC, 1, IW]
const ov::Shape image_shape = image.get_shape();
const size_t N = image_shape[1];
const size_t IW = image_shape[3];
auto image_reshape_shape = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{4},
std::vector<int64_t>{static_cast<int64_t>(N), static_cast<int64_t>(IC), 1, static_cast<int64_t>(IW)});
image = std::make_shared<ov::op::v1::Reshape>(image, image_reshape_shape, false);
}
const ov::Shape patch_sizes = {KH, KW};
const ov::Strides strides = {static_cast<size_t>(stride_h), static_cast<size_t>(stride_w)};
const ov::Shape rates = {static_cast<size_t>(dil_h), static_cast<size_t>(dil_w)};
auto pads_begin =
ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, std::vector<int64_t>{0, 0, pad_h, pad_w});
auto pads_end =
ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, std::vector<int64_t>{0, 0, pad_h, pad_w});
auto pad = std::make_shared<ov::op::v1::Pad>(image, pads_begin, pads_end, ov::op::PadMode::CONSTANT);
auto patches =
std::make_shared<ov::op::v3::ExtractImagePatches>(pad, patch_sizes, strides, rates, ov::op::PadType::VALID);
// [N, KH*KW*IC, OH, OW] → [N, OH, OW, KH*KW*IC]
auto perm1 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, std::vector<int64_t>{0, 2, 3, 1});
auto t1 = std::make_shared<ov::op::v1::Transpose>(patches, perm1);
// [N, OH, OW, KH*KW*IC] → [N, OH, OW, KH*KW, IC]
const ov::Shape out_shape = t1->get_output_shape(0);
const size_t N = out_shape[0];
const size_t OH = out_shape[1];
const size_t OW = out_shape[2];
auto reshape1_shape = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{5},
std::vector<int64_t>{static_cast<int64_t>(N), static_cast<int64_t>(OH), static_cast<int64_t>(OW),
static_cast<int64_t>(KH * KW), static_cast<int64_t>(IC)});
auto r1 = std::make_shared<ov::op::v1::Reshape>(t1, reshape1_shape, false);
// [N, OH, OW, KH*KW, IC] → [N, OH, OW, IC, KH*KW]
auto perm2 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{5}, std::vector<int64_t>{0, 1, 2, 4, 3});
auto t2 = std::make_shared<ov::op::v1::Transpose>(r1, perm2);
// flatten back to [N, OH, OW, IC*KH*KW]
auto r2_shape = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{4},
std::vector<int64_t>{static_cast<int64_t>(N), static_cast<int64_t>(OH), static_cast<int64_t>(OW),
static_cast<int64_t>(IC * KH * KW)});
res = std::make_shared<ov::op::v1::Reshape>(t2, r2_shape, false);
if (!is_2D) {
// [N, 1, OW, IC * KW] -> [1, N, OW, IC * KW]
auto final_reshape_shape = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{4},
std::vector<int64_t>{1, static_cast<int64_t>(N), static_cast<int64_t>(OW), static_cast<int64_t>(IC * KW)});
res = std::make_shared<ov::op::v1::Reshape>(res, final_reshape_shape, false);
}
auto output_type = context.get_output_type();
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_im2col_3d(const NodeContext & context) {
num_inputs_check(context, 2, 2);
const int32_t * params = context.get_output_op_params();
int32_t s0 = params[0];
int32_t s1 = params[1];
int32_t s2 = params[2];
int32_t p0 = params[3];
int32_t p1 = params[4];
int32_t p2 = params[5];
int32_t d0 = params[6];
int32_t d1 = params[7];
int32_t d2 = params[8];
int32_t IC = params[9];
ov::Output<Node> image = process_view_input_new(context, 1);
const ov::Shape kernel_shape = context.get_input(0).get_shape();
const ov::Shape image_shape = image.get_shape();
const ov::Shape out_shape = context.get_output_shape().to_shape();
const size_t KD = kernel_shape[1];
const size_t KH = kernel_shape[2];
const size_t KW = kernel_shape[3];
const size_t N = image_shape[0] / static_cast<size_t>(IC);
const size_t ID = image_shape[1];
const size_t IH = image_shape[2];
const size_t IW = image_shape[3];
const size_t OD = (ID + 2 * p2 - d2 * (KD - 1) - 1) / s2 + 1;
const size_t OH = (IH + 2 * p1 - d1 * (KH - 1) - 1) / s1 + 1;
const size_t OW = (IW + 2 * p0 - d0 * (KW - 1) - 1) / s0 + 1;
if (N == 0 || OD == 0 || OH == 0 || OW == 0) {
auto output_type = context.get_output_type();
ov::Output<Node> res = ov::op::v0::Constant::create(
output_type, ov::Shape{N * OD, OH, OW, static_cast<size_t>(IC * KD * KH * KW)}, {});
return rename_outputs_with_suffix({res}, context.get_name());
}
const size_t IH_pad = IH + 2 * p1;
const size_t IW_pad = IW + 2 * p0;
auto image_5d_shape = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{5},
std::vector<int64_t>{static_cast<int64_t>(N), static_cast<int64_t>(IC), static_cast<int64_t>(ID),
static_cast<int64_t>(IH), static_cast<int64_t>(IW)});
auto image_5d = std::make_shared<ov::op::v1::Reshape>(image, image_5d_shape, false);
auto pads_begin = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{5}, std::vector<int64_t>{0, 0, p2, p1, p0});
auto pads_end = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{5}, std::vector<int64_t>{0, 0, p2, p1, p0});
auto pad_3d = std::make_shared<ov::op::v1::Pad>(image_5d, pads_begin, pads_end, ov::op::PadMode::CONSTANT);
const ov::Shape patch_sizes = {KH, KW};
const ov::Strides strides = {static_cast<size_t>(s1), static_cast<size_t>(s0)};
const ov::Shape rates = {static_cast<size_t>(d1), static_cast<size_t>(d0)};
ov::OutputVector kd_slices;
kd_slices.reserve(KD);
auto perm_nod = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{5}, {0, 2, 1, 3, 4});
auto reshape_4d_shape = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{4},
std::vector<int64_t>{static_cast<int64_t>(N * OD), static_cast<int64_t>(IC),
static_cast<int64_t>(IH_pad), static_cast<int64_t>(IW_pad)});
auto perm1 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 2, 3, 1});
auto r1_shape = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{5},
std::vector<int64_t>{static_cast<int64_t>(N * OD), static_cast<int64_t>(OH), static_cast<int64_t>(OW),
static_cast<int64_t>(KH * KW), static_cast<int64_t>(IC)});
auto perm2 = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{5}, {0, 1, 2, 4, 3});
auto r2_shape = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{6},
std::vector<int64_t>{static_cast<int64_t>(N * OD), static_cast<int64_t>(OH), static_cast<int64_t>(OW),
static_cast<int64_t>(IC), 1, static_cast<int64_t>(KH * KW)});
auto step_c = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {static_cast<int64_t>(s2)});
auto axes_c = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {2});
for (size_t ikd = 0; ikd < KD; ++ikd) {
auto start_c = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{1}, {static_cast<int64_t>(ikd * d2)});
auto stop_c = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{1}, {static_cast<int64_t>(ikd * d2 + OD * s2)});
auto depth_slice = std::make_shared<ov::op::v8::Slice>(pad_3d, start_c, stop_c, step_c, axes_c);
auto depth_slice_trans = std::make_shared<ov::op::v1::Transpose>(depth_slice, perm_nod);
auto depth_slice_4d = std::make_shared<ov::op::v1::Reshape>(depth_slice_trans, reshape_4d_shape, false);
auto patches = std::make_shared<ov::op::v3::ExtractImagePatches>(
depth_slice_4d, patch_sizes, strides, rates, ov::op::PadType::VALID);
auto t1 = std::make_shared<ov::op::v1::Transpose>(patches, perm1);
auto r1 = std::make_shared<ov::op::v1::Reshape>(t1, r1_shape, false);
auto t2 = std::make_shared<ov::op::v1::Transpose>(r1, perm2);
auto r2 = std::make_shared<ov::op::v1::Reshape>(t2, r2_shape, false);
kd_slices.push_back(r2);
}
ov::Output<Node> res;
if (KD == 1) {
res = kd_slices[0];
} else {
res = std::make_shared<ov::op::v0::Concat>(kd_slices, 4);
}
auto final_shape = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{4},
std::vector<int64_t>{static_cast<int64_t>(N * OD), static_cast<int64_t>(OH), static_cast<int64_t>(OW),
static_cast<int64_t>(IC * KD * KH * KW)});
res = std::make_shared<ov::op::v1::Reshape>(res, final_shape, false);
auto output_type = context.get_output_type();
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,61 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/op/constant.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/maximum.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/reduce_sum.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/sqrt.hpp>
#include <openvino/op/squeeze.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_l2_norm(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input_node = process_view_input_new(context, 0);
if (context.get_op_case() == 1) {
// 92: [ 128, 16, 1, 2] VIEW q_conv-1
// [ 6144, 1, 2, 1] 0: UNARY conv_output_silu-1
// 93: [ 128, 16, 1, 2] L2_NORM q_conv_predelta-1
// [ 128, 16, 1, 2] 0: VIEW q_conv-1
auto output_shape = context.get_output_shape().to_shape();
input_node = process_view_input(context, 0, output_shape[2] * output_shape[3], 3);
input_node =
std::make_shared<ov::op::v0::Squeeze>(input_node, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
std::vector<int64_t> reshape_pattern = {0, 0, (int64_t) output_shape[2], (int64_t) output_shape[3]};
input_node = std::make_shared<ov::op::v1::Reshape>(
input_node, ov::op::v0::Constant::create(ov::element::i64, {4}, reshape_pattern), true);
}
auto squared = std::make_shared<ov::op::v1::Multiply>(input_node, input_node);
auto sum_squared = std::make_shared<ov::op::v1::ReduceSum>(
squared, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}), true);
auto l2_norm = std::make_shared<ov::op::v0::Sqrt>(sum_squared);
float eps;
memcpy(&eps, context.get_output_op_params(), sizeof(float));
auto eps_const = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {eps});
auto clamped_norm = std::make_shared<ov::op::v1::Maximum>(l2_norm, eps_const);
auto res = std::make_shared<ov::op::v1::Divide>(input_node, clamped_norm);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,27 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/op/constant.hpp>
#include <openvino/op/reduce_mean.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_mean(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1});
auto res = std::make_shared<ov::op::v1::ReduceMean>(input, axis, true);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,90 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
// Local mirror of OpenVINO's internal ov::op::internal::MOE and MOECompressed ops.
//
// The class bodies are provided by the linked libopenvino.so; only the declarations are
// needed here so the backend can construct the node directly (same approach as
// GatherMatmul and GatedDeltaNet). The class layout must stay in sync with
// openvino/src/core/dev_api/openvino/op/moe.hpp
// openvino/src/common/transformations/include/ov_ops/moe_compressed.hpp
//
// \note MOE op classes are under development and subject to change.
#pragma once
#include <optional>
#include "openvino/core/type/element_type.hpp"
#include "openvino/op/op.hpp"
namespace ov::op::internal {
class OPENVINO_API MOE : public ov::op::Op {
public:
OPENVINO_OP("MOE")
MOE() = default;
MOE(const OutputVector & args) : Op(args) {}
enum class Expert_type { GEMM2_BIAS_SWIGLU_CLAMP, GEMM3_SWIGLU };
enum class Activation_type { SWIGLU, GEGLU_TANH, GEGLU_ERF };
struct Config {
Expert_type expert_type{ Expert_type::GEMM2_BIAS_SWIGLU_CLAMP };
float expert_alpha{ 0.0f };
float expert_beta{ 1.0f };
size_t gate_idx{ 0 };
Activation_type activation_type{ Activation_type::SWIGLU };
};
MOE(const OutputVector & args, const Config & config);
const Config & get_config() const;
void set_config(const Config & config);
bool visit_attributes(AttributeVisitor & visitor) override;
void validate_and_infer_types() override;
std::shared_ptr<Node> clone_with_new_inputs(const OutputVector & new_args) const override;
private:
Config m_config;
};
class OPENVINO_API MOECompressed : public MOE {
public:
OPENVINO_OP("MOECompressed", "", ov::op::internal::MOE)
MOECompressed() = default;
struct Config : public MOE::Config {
size_t hidden_size = 0;
size_t inter_size = 0;
size_t num_expert = 0;
size_t num_shared_expert = 0;
size_t top_k = 0;
// numeric_limits<size_t>::max() means per_channel compression (single group)
size_t group_size = 0;
bool has_batch_dim = false;
bool has_zp = false;
ov::element::Type out_type = ov::element::dynamic;
std::optional<float> scale_factor;
};
MOECompressed(const OutputVector & args, const Config & config);
const Config & get_config() const { return m_config; }
void set_scale_factor(float scale_factor) { m_config.scale_factor = scale_factor; }
bool visit_attributes(AttributeVisitor & visitor) override;
void validate_and_infer_types() override;
std::shared_ptr<Node> clone_with_new_inputs(const OutputVector & new_args) const override;
protected:
Config m_config;
};
} // namespace ov::op::internal
@@ -0,0 +1,254 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include "gather_matmul.hpp"
#include "ggml-openvino/ggml-openvino-extra.h"
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <openvino/op/bitwise_and.hpp>
#include <openvino/op/bitwise_right_shift.hpp>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
namespace {
std::shared_ptr<ov::op::v0::Constant> const_i64(const std::vector<int64_t> & values) {
return ov::op::v0::Constant::create(ov::element::i64, ov::Shape{values.size()}, values);
}
ov::Output<ov::Node> slice_axis(const ov::Output<ov::Node> & input, int64_t axis, int64_t begin, int64_t end) {
return std::make_shared<ov::op::v8::Slice>(input, const_i64({begin}), const_i64({end}), const_i64({1}),
const_i64({axis}));
}
// GGML tensors are rank 4, but stateful models drop the leading size-1 batch dim, so
// activations and ids arrive one rank lower. Pick the trailing dims by actual rank.
std::vector<int> trailing_dims(const ov::Output<ov::Node> & input, int count) {
const auto rank = input.get_partial_shape().rank();
FRONT_END_OP_CONVERSION_CHECK(rank.is_static(), "Expected static rank for MUL_MAT_ID input");
const int rank_len = static_cast<int>(rank.get_length());
FRONT_END_OP_CONVERSION_CHECK(rank_len >= count, "MUL_MAT_ID input rank is too low");
std::vector<int> dims;
for (int i = rank_len - count; i < rank_len; ++i) {
dims.push_back(i);
}
return dims;
}
ov::Output<ov::Node> static_shape_dims_or_shapeof(const ov::Output<ov::Node> & input,
const std::vector<int> & dims) {
const auto & partial_shape = input.get_partial_shape();
if (partial_shape.is_static()) {
std::vector<int64_t> values;
values.reserve(dims.size());
for (const int64_t dim : dims) {
values.push_back(partial_shape[dim].get_length());
}
return const_i64(values);
}
auto shape = std::make_shared<ov::op::v3::ShapeOf>(input, ov::element::i64);
return get_dimensions(shape, dims);
}
ov::Output<ov::Node> translate_mul_mat_id_mxfp4_packed(const NodeContext & context,
ov::Output<ov::Node> expert_weights,
ov::Output<ov::Node> activations,
ov::Output<ov::Node> ids) {
auto packed_shape = expert_weights.get_partial_shape().to_shape();
FRONT_END_OP_CONVERSION_CHECK(packed_shape.size() == 5 && packed_shape[4] == 17,
"Expected packed MXFP4 expert weights with shape [1, n_expert, m, k_blocks, 17]");
const int64_t n_expert = static_cast<int64_t>(packed_shape[1]);
const int64_t rows = static_cast<int64_t>(packed_shape[2]);
const int64_t k_blocks = static_cast<int64_t>(packed_shape[3]);
const int64_t qk = 32;
const int64_t cols = k_blocks * qk;
auto packed_shape_4d = const_i64({n_expert, rows, k_blocks, 17});
expert_weights = std::make_shared<ov::op::v1::Reshape>(expert_weights, packed_shape_4d, false);
auto activations_shape_4d = std::make_shared<ov::op::v3::ShapeOf>(activations, ov::element::i64);
auto ids_shape_4d = std::make_shared<ov::op::v3::ShapeOf>(ids, ov::element::i64);
auto activations_shape_3d = get_dimensions(activations_shape_4d, {1, 2, 3});
auto ids_shape_2d = get_dimensions(ids_shape_4d, {2, 3});
activations = std::make_shared<ov::op::v1::Reshape>(activations, activations_shape_3d, false);
ids = std::make_shared<ov::op::v1::Reshape>(ids, ids_shape_2d, false);
if (ids.get_element_type() != ov::element::i32 && ids.get_element_type() != ov::element::i64) {
ids = std::make_shared<ov::op::v0::Convert>(ids, ov::element::i32);
}
auto gather_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {0});
static const std::vector<float> f4e2m1_lut = {0.0f, 0.5f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f,
-0.0f, -0.5f, -1.0f, -1.5f, -2.0f, -3.0f, -4.0f, -6.0f};
std::vector<float> e8m0_lut(256);
for (size_t i = 0; i < e8m0_lut.size(); ++i) {
uint32_t bits = static_cast<uint32_t>(i) << 23;
memcpy(&e8m0_lut[i], &bits, sizeof(float));
}
e8m0_lut[0] = std::numeric_limits<float>::min() / 2.0f;
e8m0_lut[255] = std::numeric_limits<float>::quiet_NaN();
auto f4_lut = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{f4e2m1_lut.size()}, f4e2m1_lut);
auto scale_lut = ov::op::v0::Constant::create(ov::element::f32, ov::Shape{e8m0_lut.size()}, e8m0_lut);
auto selected_packed_weights = std::make_shared<ov::op::v8::Gather>(expert_weights, ids, gather_axis);
auto scale_byte = slice_axis(selected_packed_weights, 4, 0, 1);
auto qs = slice_axis(selected_packed_weights, 4, 1, 17);
auto low = std::make_shared<ov::op::v13::BitwiseAnd>(
qs, ov::op::v0::Constant::create(ov::element::u8, ov::Shape{}, {0x0F}), ov::op::AutoBroadcastType::NUMPY);
auto high_shift = std::make_shared<ov::op::v15::BitwiseRightShift>(
qs, ov::op::v0::Constant::create(ov::element::u8, ov::Shape{}, {4}), ov::op::AutoBroadcastType::NUMPY);
auto nibbles = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{low, high_shift}, 4);
auto nibble_indices = std::make_shared<ov::op::v0::Convert>(nibbles, ov::element::i32);
auto weights_f32 = std::make_shared<ov::op::v8::Gather>(f4_lut, nibble_indices, gather_axis);
auto scale_indices = std::make_shared<ov::op::v0::Convert>(scale_byte, ov::element::i32);
auto scales_f32 = std::make_shared<ov::op::v8::Gather>(scale_lut, scale_indices, gather_axis);
ov::Output<ov::Node> selected_weights = std::make_shared<ov::op::v1::Multiply>(weights_f32, scales_f32,
ov::op::AutoBroadcastType::NUMPY);
auto ids_shape = std::make_shared<ov::op::v3::ShapeOf>(ids, ov::element::i64);
auto selected_weights_target_dims = std::make_shared<ov::op::v0::Concat>(
ov::OutputVector{get_dimensions(ids_shape, {0, 1}), const_i64({rows, cols})}, 0);
selected_weights = std::make_shared<ov::op::v1::Reshape>(selected_weights, selected_weights_target_dims, false);
auto activations_shape = std::make_shared<ov::op::v3::ShapeOf>(activations, ov::element::i64);
ov::Output<ov::Node> acts_target_dims = std::make_shared<ov::op::v0::Concat>(
ov::OutputVector{
get_dimensions(activations_shape, {0}),
get_dimensions(ids_shape, {1}),
get_dimensions(activations_shape, {2}),
},
0);
ov::Output<ov::Node> acts_broadcasted =
std::make_shared<ov::op::v3::Broadcast>(activations, acts_target_dims, ov::op::BroadcastType::BIDIRECTIONAL);
auto activations_expanded = std::make_shared<ov::op::v0::Unsqueeze>(acts_broadcasted, const_i64({2}));
ov::Output<ov::Node> result =
std::make_shared<ov::op::v0::MatMul>(activations_expanded, selected_weights, false, true);
auto batch_dim = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto row_dim = ov::op::v0::Constant::create(ov::element::i64, {1}, {rows});
auto result_target_dims = std::make_shared<ov::op::v0::Concat>(
ov::OutputVector{batch_dim, get_dimensions(ids_shape, {0, 1}), row_dim}, 0);
result = std::make_shared<ov::op::v1::Reshape>(result, result_target_dims, false);
const auto output_type = context.get_output_type();
if (result.get_element_type() != output_type) {
result = std::make_shared<ov::op::v0::Convert>(result, output_type);
}
return result;
}
} // namespace
OutputVector translate_mul_mat_id(const NodeContext & context) {
num_inputs_check(context, 3, 3);
auto expert_weights = process_view_input_new(context, 0);
auto activations = process_view_input_new(context, 1);
auto ids = process_view_input_new(context, 2);
if (activations.get_partial_shape().rank() == 3) {
activations = std::make_shared<ov::op::v0::Unsqueeze>(activations, const_i64({0}));
}
if (ids.get_partial_shape().rank() == 3) {
ids = std::make_shared<ov::op::v0::Unsqueeze>(ids, const_i64({0}));
}
if (expert_weights.get_element_type() == ov::element::u8 && expert_weights.get_partial_shape().rank().is_static() &&
expert_weights.get_partial_shape().rank().get_length() == 5) {
return rename_outputs_with_suffix({translate_mul_mat_id_mxfp4_packed(context, expert_weights, activations, ids)},
context.get_name());
}
// General (non-packed) path: dense F32/F16/BF16 weights, or the f16 dequantization chain for
// quantized MoE experts (see extract_quantized_weights / make_int4_weights / make_int8_weights in
// ggml-quants.cpp). Routed through ov::op::internal::GatherMatmul instead of a naive
// Gather+Broadcast+MatMul, so the selected expert's full weight matrix is never materialized per
// token. The CPU plugin's ConvertGatherMatmulToGatherMatmulCompressed pass (run during
// compile_model) fuses the dequantization chain feeding GatherMatmul's B input into a
// GatherMatmulCompressed node automatically, as long as MarkDequantization has marked the chain --
// see translate_session.cpp's apply_transformations for the MarkDequantization registration.
//
// OpenVINO sees GGML tensors in reversed dimension order:
// activations: [1, n_tokens, n_used_or_1, k]
// ids: [1, 1, n_tokens, n_used]
// expert_weights is either [1, n_expert, m, k] (4D, e.g. non-quantized weights without a
// pre-built extra) or already [n_expert, m, k] (3D, weights routed through
// process_weight_tensor) -- GatherMatmul's B input expects the latter.
auto expert_weights_rank = expert_weights.get_partial_shape().rank();
FRONT_END_OP_CONVERSION_CHECK(expert_weights_rank.is_static(),
"Expected static rank for MUL_MAT_ID expert weights");
if (expert_weights_rank.get_length() == 4) {
auto expert_weights_shape_3d = static_shape_dims_or_shapeof(expert_weights, {1, 2, 3});
expert_weights = std::make_shared<ov::op::v1::Reshape>(expert_weights, expert_weights_shape_3d, false);
}
auto activations_shape_3d = static_shape_dims_or_shapeof(activations, trailing_dims(activations, 3));
auto ids_shape_2d = static_shape_dims_or_shapeof(ids, trailing_dims(ids, 2));
activations = std::make_shared<ov::op::v1::Reshape>(activations, activations_shape_3d, false);
ids = std::make_shared<ov::op::v1::Reshape>(ids, ids_shape_2d, false);
if (ids.get_element_type() != ov::element::i32 && ids.get_element_type() != ov::element::i64) {
ids = std::make_shared<ov::op::v0::Convert>(ids, ov::element::i32);
}
const auto output_type = context.get_output_type();
const auto activations_type = ggml_openvino_is_gpu() ? ov::element::f16 : ov::element::f32;
if (activations.get_element_type() != activations_type) {
activations = std::make_shared<ov::op::v0::Convert>(activations, activations_type);
}
// GatherMatmul's A input is [n_used_or_1, n_tokens, k]; activations_3d is
// [n_tokens, n_used_or_1, k].
auto activations_transpose_order = const_i64({1, 0, 2});
ov::Output<ov::Node> activations_for_gather =
std::make_shared<ov::op::v1::Transpose>(activations, activations_transpose_order);
ov::Output<ov::Node> result = std::make_shared<ov::op::internal::GatherMatmul>(activations_for_gather, expert_weights, ids);
// result is [n_used, n_tokens, m]; GGML expects [1, n_tokens, n_used, m], except on the
// stateful path where the leading batch dim is dropped.
auto result_transpose_order = const_i64({1, 0, 2});
result = std::make_shared<ov::op::v1::Transpose>(result, result_transpose_order);
if (!context.is_stateful()) {
auto unsqueeze_axes = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
result = std::make_shared<ov::op::v0::Unsqueeze>(result, unsqueeze_axes);
}
if (result.get_element_type() != output_type) {
result = std::make_shared<ov::op::v0::Convert>(result, output_type);
}
return rename_outputs_with_suffix({result}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,92 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <climits>
#include <cstdint>
#include <memory>
#include <openvino/core/node.hpp>
#include <openvino/core/node_output.hpp>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <openvino/op/util/op_types.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_mulmat(const NodeContext & context) {
num_inputs_check(context, 2, 2);
int op_case = context.get_op_case();
ov::Output<ov::Node> B = process_view_input_new(context, 0);
ov::Output<ov::Node> A = process_view_input_new(context, 1);
if (A.get_element_type() != B.get_element_type()) {
B = std::make_shared<ov::op::v0::Convert>(B, context.get_input_type(1));
}
auto B_shape = context.get_input_shape(0).to_shape();
auto A_shape = context.get_input_shape(1).to_shape();
int64_t A_batch = A_shape[1];
int64_t B_batch = B_shape[1];
auto A_batch_larger = A_batch > B_batch;
auto batch_large = A_batch_larger ? A_batch : B_batch;
auto batch_small = A_batch_larger ? B_batch : A_batch;
Output<Node> Z = A_batch_larger ? B : A;
auto Z_shape = A_batch_larger ? B_shape : A_shape;
int64_t factor = batch_large / batch_small;
if (factor > 1 && batch_small > 1) {
auto batch_large_node = ov::op::v0::Constant::create(ov::element::i64, {1}, std::vector<int64_t>{batch_large});
auto batch_small_node = ov::op::v0::Constant::create(ov::element::i64, {1}, std::vector<int64_t>{batch_small});
auto factor_node = ov::op::v0::Constant::create(ov::element::i64, {1}, std::vector<int64_t>{factor});
auto unsqueeze_axes = ov::op::v0::Constant::create(ov::element::i64, Shape{}, {2});
auto Z_unsqueezed = std::make_shared<ov::op::v0::Unsqueeze>(Z, unsqueeze_axes);
auto broadcast_shape = ov::op::v0::Constant::create(
ov::element::i64, {5}, {(int64_t) 1, (int64_t) 1, factor, (int64_t) 1, (int64_t) 1});
auto new_Z_shape = ov::op::v0::Constant::create(ov::element::i64, {4},
{(int64_t) 0, batch_large, (int64_t) -1, (int64_t) Z_shape[3]});
if (op_case == 2) {
new_Z_shape = ov::op::v0::Constant::create(ov::element::i64, {4},
{(int64_t) 0, batch_large, (int64_t) Z_shape[2], (int64_t) -1});
}
auto Z_broadcasted = std::make_shared<ov::op::v3::Broadcast>(Z_unsqueezed, broadcast_shape,
ov::op::BroadcastType::BIDIRECTIONAL);
Z = std::make_shared<ov::op::v1::Reshape>(Z_broadcasted, new_Z_shape, true);
}
if (A_batch_larger) {
B = Z;
} else {
A = Z;
}
bool transpose_b = true;
ov::Output<Node> res = std::make_shared<ov::op::v0::MatMul>(A, B, false, transpose_b);
const auto output_type = context.get_output_type();
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,31 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <cstring>
#include <memory>
#include <openvino/op/constant.hpp>
#include <openvino/op/mvn.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_norm(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input_node = process_view_input_new(context, 0);
float eps;
memcpy(&eps, context.get_output_op_params(), sizeof(float));
auto axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1});
auto res = std::make_shared<ov::op::v6::MVN>(input_node, axes, true, eps, ov::op::MVNEpsMode::INSIDE_SQRT);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,94 @@
#include "../op_table.h"
#include "../utils.h"
#include <array>
#include <cstdint>
#include <openvino/op/constant.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/pad.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <utility>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
namespace {
ov::Output<ov::Node> translate_circular_pad(ov::Output<ov::Node> input,
const std::array<int32_t, 8> & pads,
const ov::Shape & input_shape) {
ov::Output<ov::Node> result = std::move(input);
const std::array<int32_t, 4> pads_begin = {pads[6], pads[4], pads[2], pads[0]};
const std::array<int32_t, 4> pads_end = {pads[7], pads[5], pads[3], pads[1]};
for (size_t axis = 0; axis < input_shape.size(); ++axis) {
const int64_t input_dim = static_cast<int64_t>(input_shape[axis]);
const int64_t pad_begin = pads_begin[axis];
const int64_t pad_end = pads_end[axis];
if (pad_begin == 0 && pad_end == 0) {
continue;
}
FRONT_END_CHECK_IMPLEMENTED(input_dim > 0, "Circular PAD requires static non-zero input dimensions");
std::vector<int64_t> indices(static_cast<size_t>(input_dim + pad_begin + pad_end));
for (int64_t index = 0; index < static_cast<int64_t>(indices.size()); ++index) {
int64_t wrapped = (index - pad_begin) % input_dim;
if (wrapped < 0) {
wrapped += input_dim;
}
indices[static_cast<size_t>(index)] = wrapped;
}
auto gather_indices = ov::op::v0::Constant::create(ov::element::i64, {indices.size()}, indices);
auto gather_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {axis});
result = std::make_shared<ov::op::v8::Gather>(result, gather_indices, gather_axis);
}
return result;
}
} // namespace
OutputVector translate_pad(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
if (context.get_input_shape(0) == context.get_output_shape()) {
return rename_outputs_with_suffix({input}, context.get_name());
}
const int32_t * op_params = context.get_output_op_params();
FRONT_END_CHECK_IMPLEMENTED(op_params != nullptr, "PAD requires output op params");
const std::array<int32_t, 8> pads = {op_params[0], op_params[1], op_params[2], op_params[3],
op_params[4], op_params[5], op_params[6], op_params[7]};
const bool circular = op_params[8] != 0;
if (circular) {
auto res = translate_circular_pad(input, pads, context.get_input_shape(0).to_shape());
return rename_outputs_with_suffix({res}, context.get_name());
}
const std::vector<int64_t> pads_begin = {pads[6], pads[4], pads[2], pads[0]};
const std::vector<int64_t> pads_end = {pads[7], pads[5], pads[3], pads[1]};
auto pads_begin_node = ov::op::v0::Constant::create(ov::element::i64, {pads_begin.size()}, pads_begin);
auto pads_end_node = ov::op::v0::Constant::create(ov::element::i64, {pads_end.size()}, pads_end);
auto pad_value = ov::op::v0::Constant::create(context.get_input_type(0), ov::Shape{}, {0});
auto res =
std::make_shared<ov::op::v1::Pad>(input, pads_begin_node, pads_end_node, pad_value, ov::op::PadMode::CONSTANT);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,159 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <climits>
#include <cstdint>
#include <memory>
#include <openvino/core/node.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/transpose.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_permute(const NodeContext & context) {
num_inputs_check(context, 1, 1);
int op_case = context.get_op_case();
FRONT_END_CHECK_IMPLEMENTED(op_case != 0, "Unsupported PERMUTE case");
// op_case 1 is trivial permute
// op_case 2 is to permute Q. It has a preceding VIEW that reshapes Q to restore the sequqence dimension
// op_case 3 4 it to permute KV cache in the default layout
// op_case 5 6 is to permute V cache when `-fa off`, where v_trans=true
ov::Output<Node> res;
ov::Output<Node> src;
if (op_case == 3 || op_case == 4 || op_case == 5 || op_case == 6) {
src = context.get_input(0);
} else {
src = process_view_input_new(context, 0);
}
std::vector<int64_t> perm_values{0, 2, 1, 3};
const int32_t * op_params = context.get_output_op_params();
if (op_params != nullptr) {
for (size_t input_axis = 0; input_axis < perm_values.size(); ++input_axis) {
const size_t output_axis = static_cast<size_t>(op_params[input_axis]);
perm_values[perm_values.size() - 1 - output_axis] =
static_cast<int64_t>(perm_values.size() - 1 - input_axis);
}
}
if (op_case == 1 || context.is_stateful()) {
// The stateful path carries hidden-state tensors in a rank-3 layout (the
// leading batch dim is dropped, e.g. Gemma4's per-layer-embedding path). The
// perm above is rank-4; when the actual input is rank-3, drop the batch axis
// (perm[0], which is always the identity 0 here) and shift the rest down by 1
// so the transpose order matches the input rank.
std::vector<int64_t> perm_used = perm_values;
const auto & src_ps = src.get_partial_shape();
if (src_ps.rank().is_static() && src_ps.rank().get_length() == 3 && perm_values.size() == 4 &&
perm_values[0] == 0) {
perm_used = {perm_values[1] - 1, perm_values[2] - 1, perm_values[3] - 1};
}
auto perm = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{perm_used.size()}, perm_used);
res = std::make_shared<ov::op::v1::Transpose>(src, perm);
} else if (op_case == 2) {
auto perm = ov::op::v0::Constant::create(ov::element::i64, {4}, perm_values);
auto output_shape = context.get_output_shape().to_shape();
auto n_heads = ov::op::v0::Constant::create(ov::element::i64, {1}, {output_shape[1]});
auto head_size = ov::op::v0::Constant::create(ov::element::i64, {1}, {output_shape[3]});
auto n_seq_active = context.has_input("n_seq_active") ?
context.get_input("n_seq_active") :
ov::op::v0::Constant::create(ov::element::i64, {1}, {output_shape[0]});
auto neg_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto new_shape =
std::make_shared<ov::op::v0::Concat>(ov::OutputVector{n_seq_active, neg_one, n_heads, head_size}, 0);
// // Alternative
// auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
// auto new_shape = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{n_seq_active, neg_one, zero, zero}, 0);
auto reshaped = std::make_shared<ov::op::v1::Reshape>(src, new_shape, true);
res = std::make_shared<ov::op::v1::Transpose>(reshaped, perm);
} else {
auto perm = ov::op::v0::Constant::create(ov::element::i64, {4}, perm_values);
auto cache_shape = src.get_partial_shape();
auto output_shape = context.get_output_shape().to_shape();
int64_t head_size = output_shape[3];
int64_t n_heads = output_shape[1];
if (op_case == 5 || op_case == 6) {
head_size = output_shape[2];
n_heads = output_shape[1];
}
int64_t ctx_per_seq = cache_shape[2].is_static() ? cache_shape[2].get_length() : -1;
int64_t n_seq = cache_shape[1].get_length();
Output<Node> attention_size;
if (!context.has_input("attention_size")) {
attention_size = ov::op::v0::Constant::create(ov::element::i64, {1}, {output_shape[2]});
} else if (op_case == 3 || op_case == 5) {
attention_size = context.get_input("attention_size");
} else {
attention_size = context.get_input("attention_size_swa");
}
Output<Node> seq_active_start;
Output<Node> seq_active_end;
if (context.has_input("seq_active_start")) {
seq_active_start = context.get_input("seq_active_start");
seq_active_end = context.get_input("seq_active_end");
} else {
int64_t n_seq_active = output_shape[0];
size_t offset = *((size_t *) context.get_input_op_params(0));
int64_t seq_active_start_val = offset / context.get_input_stride(0)[0];
int64_t seq_active_end_val = seq_active_start_val + n_seq_active;
seq_active_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {seq_active_start_val});
seq_active_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {seq_active_end_val});
}
// 1. reshape to [n_seq, ctx_per_seq, n_heads, head_size] (for `-fa off` [n_seq, n_heads, head_size, ctx_per_seq])
// 2. slice out the active sequences
// 3. slice out the attention part in each sequence
// 4. permute (skip for `-fa off`)
auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
if (op_case == 3 || op_case == 4) {
auto src_reshaped = std::make_shared<ov::op::v1::Reshape>(
src, ov::op::v0::Constant::create(ov::element::i64, {4}, {n_seq, ctx_per_seq, n_heads, head_size}),
false);
ov::Output<ov::Node> after_seq_slice;
if (n_seq == 1) {
after_seq_slice = src_reshaped;
} else {
after_seq_slice =
std::make_shared<ov::op::v8::Slice>(src_reshaped, seq_active_start, seq_active_end, one, zero);
}
auto slice2 = std::make_shared<ov::op::v8::Slice>(after_seq_slice, zero, attention_size, one, one);
res = std::make_shared<ov::op::v1::Transpose>(slice2, perm);
} else {
auto three = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
auto src_reshaped = std::make_shared<ov::op::v1::Reshape>(
src, ov::op::v0::Constant::create(ov::element::i64, {4}, {n_seq, n_heads, head_size, ctx_per_seq}),
false);
ov::Output<ov::Node> after_seq_slice;
if (n_seq == 1) {
after_seq_slice = src_reshaped;
} else {
after_seq_slice =
std::make_shared<ov::op::v8::Slice>(src_reshaped, seq_active_start, seq_active_end, one, zero);
}
auto slice2 = std::make_shared<ov::op::v8::Slice>(after_seq_slice, zero, attention_size, one, three);
res = slice2;
}
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,53 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/avg_pool.hpp>
#include <openvino/op/max_pool.hpp>
#include <openvino/op/convert.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_pool_2d(const NodeContext & context) {
num_inputs_check(context, 1, 1);
const int32_t * params = context.get_output_op_params();
const int k0 = params[1];
const int k1 = params[2];
const int s0 = params[3];
const int s1 = params[4];
const int p0 = params[5];
const int p1 = params[6];
const int op_case = context.get_op_case();
ov::Output<Node> input = context.get_input(0);
ov::Strides strides{static_cast<size_t>(s1), static_cast<size_t>(s0)};
ov::Shape pads_begin{static_cast<size_t>(p1), static_cast<size_t>(p0)};
ov::Shape pads_end{static_cast<size_t>(p1), static_cast<size_t>(p0)};
ov::Shape kernel{static_cast<size_t>(k1), static_cast<size_t>(k0)};
ov::Output<Node> res;
switch (op_case) {
case 1: // GGML_OP_POOL_MAX
{
res = std::make_shared<ov::op::v1::MaxPool>(input, strides, pads_begin, pads_end, kernel);
break;
}
case 2: // GGML_OP_POOL_AVG
{
res = std::make_shared<ov::op::v1::AvgPool>(input, strides, pads_begin, pads_end, kernel, false);
break;
}
default:
break;
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,47 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/tile.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML_OP_REPEAT tiles src[0] to fill the destination shape. Every destination
// dimension is an integer multiple of the corresponding source dimension.
OutputVector translate_repeat(const NodeContext & context) {
num_inputs_check(context, 1, 2);
auto input = process_view_input_new(context, 0);
const auto input_shape = context.get_input_shape(0).to_shape();
const auto output_shape = context.get_output_shape().to_shape();
std::vector<int64_t> repeats(4, 1);
for (size_t axis = 0; axis < 4; ++axis) {
const int64_t input_dim = input_shape[axis];
const int64_t output_dim = output_shape[axis];
FRONT_END_OP_CONVERSION_CHECK(input_dim > 0 && output_dim > 0 && output_dim % input_dim == 0,
"REPEAT input shape ", input_shape, " cannot tile to match ", output_shape);
repeats[axis] = output_dim / input_dim;
}
auto repeats_node = ov::op::v0::Constant::create(ov::element::i64, {repeats.size()}, repeats);
ov::Output<ov::Node> res = std::make_shared<ov::op::v0::Tile>(input, repeats_node);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,86 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <cstdint>
#include <memory>
#include <openvino/core/node.hpp>
#include <openvino/core/node_output.hpp>
#include <openvino/frontend/exception.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/reshape.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_reshape(const NodeContext & context) {
num_inputs_check(context, 1, 1);
if (context.get_input(0).get_partial_shape().is_static() &&
context.get_input_shape(0) == context.get_output_shape()) {
return {context.get_input(0)};
}
const int op_case = context.get_op_case();
const auto output_shape = context.get_output_shape().to_shape();
std::vector<int64_t> shape(output_shape.begin(), output_shape.end());
std::shared_ptr<ov::Node> new_shape_node;
switch (op_case) {
case 0:
break;
case 1:
case 9:
shape[1] = -1;
if (context.is_stateful() && op_case == 1) {
shape.erase(shape.begin());
}
break;
case 2:
case 3:
shape[2] = -1;
if (op_case == 3) {
shape[3] = 1;
}
break;
case 4:
return {context.get_input(0).get_node_shared_ptr()->input_value(0)};
case 5:
case 7:
shape = {1, 1, -1, shape[3]};
if (context.is_stateful() && op_case == 5) {
shape.erase(shape.begin());
}
break;
case 6:
// Recurrent inputs keep the active sequence count separate from the token count.
if (context.has_input("s_copy_active_slot_len")) {
auto n_slot_active_len = context.get_input("s_copy_active_slot_len");
auto emb_size = ov::op::v0::Constant::create(ov::element::i64, {1}, {shape[3]});
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto neg_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
new_shape_node =
std::make_shared<ov::op::v0::Concat>(ov::OutputVector{one, n_slot_active_len, neg_one, emb_size}, 0);
} else {
shape = {1, 1, -1, shape[3]};
}
break;
case 8:
shape[0] = -1;
break;
default:
FRONT_END_OP_CONVERSION_CHECK(false, "Unsupported RESHAPE case: ", op_case);
}
if (!new_shape_node) {
new_shape_node = ov::op::v0::Constant::create(ov::element::i64, {shape.size()}, shape);
}
auto res = std::make_shared<ov::op::v1::Reshape>(context.get_input(0), new_shape_node, false);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,89 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/op/add.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/negative.hpp>
#include <openvino/op/power.hpp>
#include <openvino/op/reduce_mean.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/sqrt.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_rms_norm(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto op_case = context.get_op_case();
ov::Output<ov::Node> input_node;
if (op_case == 3) {
// Flatten sequence and token dimensions to match the gate layout.
auto input_shape = context.get_input_shape(0).to_shape();
input_node = std::make_shared<ov::op::v1::Reshape>(
context.get_input(0),
ov::op::v0::Constant::create(
ov::element::i64, {4}, std::vector<int64_t>{1, -1, (int64_t) input_shape[2], (int64_t) input_shape[3]}),
false);
} else if (op_case == 1) {
input_node = process_view_input_new(context, 0);
} else if (op_case == 2) {
auto ssm_state_size = context.get_ssm_state_size();
// The GDN op packs [attn | new_state] along the row axis; the state occupies the last
// ssm_state_size * n_seqs rows. Slice it off (scaling by the active sequence count) to keep
// just the attention output.
ov::Output<ov::Node> state_end;
if (context.has_input("s_copy_active_slot_len")) {
auto len = context.get_input("s_copy_active_slot_len");
auto state_rows = std::make_shared<ov::op::v1::Multiply>(
ov::op::v0::Constant::create(ov::element::i64, {1}, {ssm_state_size}), len);
state_end = std::make_shared<ov::op::v0::Negative>(state_rows);
} else {
state_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {-ssm_state_size});
}
auto gdn_attn_output = std::make_shared<ov::op::v8::Slice>(
context.get_input(0), ov::op::v0::Constant::create(ov::element::i64, {1}, {0}), state_end,
ov::op::v0::Constant::create(ov::element::i64, {1}, {1}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {2}));
auto input_shape = context.get_input_shape(0).to_shape();
input_node = std::make_shared<ov::op::v1::Reshape>(
gdn_attn_output,
ov::op::v0::Constant::create(
ov::element::i64, {4}, std::vector<int64_t>{1, -1, (int64_t) input_shape[2], (int64_t) input_shape[3]}),
false);
} else {
input_node = process_view_input_new(context, 0);
}
auto square = std::make_shared<ov::op::v1::Multiply>(input_node, input_node);
auto mean = std::make_shared<ov::op::v1::ReduceMean>(
square, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}), true);
float eps;
memcpy(&eps, context.get_output_op_params(), sizeof(float));
auto rms = std::make_shared<ov::op::v0::Sqrt>(
std::make_shared<ov::op::v1::Add>(mean, ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {eps})));
auto reciprocal =
std::make_shared<ov::op::v1::Divide>(ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {1.0f}), rms);
auto res = std::make_shared<ov::op::v1::Multiply>(input_node, reciprocal);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,36 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/roll.hpp>
#include <openvino/op/constant.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_roll(const NodeContext & context) {
num_inputs_check(context, 1, 1);
const int32_t * params = context.get_output_op_params();
int64_t s0 = params[0];
int64_t s1 = params[1];
int64_t s2 = params[2];
int64_t s3 = params[3];
auto input = context.get_input(0);
auto shift = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{4}, std::vector<int64_t>{s3, s2, s1, s0});
auto axes = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{4}, std::vector<int64_t>{0, 1, 2, 3});
auto roll = std::make_shared<ov::op::v7::Roll>(input, shift, axes);
return rename_outputs_with_suffix({roll}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,245 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <cstdint>
#include <memory>
#include <openvino/core/node.hpp>
#include <openvino/core/node_output.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/split.hpp>
#include <openvino/op/subtract.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <openvino/op/variadic_split.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_rope(const NodeContext & context) {
num_inputs_check(context, 2, 3);
int op_case = context.get_op_case();
ov::Output<Node> res;
auto data_node = process_view_input_new(context, 0).get_node_shared_ptr();
auto output_shape = context.get_output_shape().to_shape();
int32_t * op_params = context.get_output_op_params();
const int mode = op_case;
const int64_t head_dim = static_cast<int64_t>(output_shape[3]);
const int64_t configured_n_dims = static_cast<int64_t>(op_params[1]);
const int64_t n_dims = configured_n_dims == 0 ? head_dim : configured_n_dims;
const int64_t n_offs = static_cast<int64_t>(op_params[15]);
constexpr int TYPE_NORMAL = 0;
constexpr int TYPE_NEOX = 1;
constexpr int TYPE_IMROPE = 2;
constexpr int TYPE_VISION = 3;
constexpr int TYPE_MROPE = 4;
Output<Node> cos_theta_node;
Output<Node> sin_theta_node;
if (context.has_input("rope_cos")) {
cos_theta_node = context.get_input("rope_cos");
sin_theta_node = context.get_input("rope_sin");
} else {
std::string cache_key = "rope_sin_cos";
for (int i = 0; i < 15; i++) {
cache_key += "_" + std::to_string(op_params[i]);
}
if (context.get_input_size() == 3) {
cache_key += "_ff_" + context.get_input_names()[2];
}
if (context.has_input(cache_key + "_cos")) {
cos_theta_node = context.get_input(cache_key + "_cos");
sin_theta_node = context.get_input(cache_key + "_sin");
} else {
auto inp_pos = context.get_input(1).get_node_shared_ptr();
std::shared_ptr<ov::Node> rope_freqs_weight;
if (context.get_input_size() == 3) {
rope_freqs_weight = context.get_input(2).get_node_shared_ptr();
}
auto sin_cos = make_sin_cos(op_params, inp_pos, rope_freqs_weight, mode, false, head_dim);
sin_theta_node = sin_cos.first;
cos_theta_node = sin_cos.second;
context.put_shared(cache_key + "_cos", cos_theta_node);
context.put_shared(cache_key + "_sin", sin_theta_node);
}
}
auto output_type = context.get_output_type();
if (data_node->get_element_type() != ov::element::f32) {
data_node = std::make_shared<ov::op::v0::Convert>(data_node, ov::element::f32);
}
const int64_t total_rope_dims = (mode == TYPE_VISION) ? (2 * n_dims) : n_dims;
FRONT_END_OP_CONVERSION_CHECK(n_offs >= 0 && (n_offs % 2 == 0),
"ROPE expects non-negative even n_offs");
FRONT_END_OP_CONVERSION_CHECK(n_dims > 0 && total_rope_dims + n_offs <= head_dim && (n_dims % 2 == 0),
"ROPE expects even n_dims with total_rope_dims + n_offs <= head_dim");
// RoPEFusionFlux requires rank_equals(4) on x, t_cos and t_sin. The cos/sin
// tables are already built rank-4 ([1, S, 1, head_size/2]) for both modes. In
// stateful mode the data arrives rank-3 ([S, n_heads, head_size]), so lift it
// to rank-4 ([1, S, n_heads, head_size]) here. Stateful RoPE already produced
// rank-4 output, so downstream attention is unaffected.
if (context.is_stateful()) {
const int64_t batch = static_cast<int64_t>(output_shape[0]);
auto r4_shape = ov::op::v0::Constant::create(
ov::element::i64, {4},
std::vector<int64_t>{batch, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]});
data_node = std::make_shared<ov::op::v1::Reshape>(data_node, r4_shape, false);
}
// For TYPE_NORMAL rope (both stateful and stateless) we emit the Flux-style
// interleaved pattern below so the GPU plugin's RoPEFusionFlux matcher folds it
// into ov::op::internal::RoPE.
if (mode == TYPE_NORMAL) {
auto axis_last = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto step_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
const int64_t batch = static_cast<int64_t>(output_shape[0]);
const int64_t n_heads = static_cast<int64_t>(output_shape[2]);
const int64_t half = n_dims / 2;
auto rot_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs});
auto rot_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs + n_dims});
auto rot_data = std::make_shared<ov::op::v8::Slice>(data_node, rot_start, rot_end, step_one, axis_last);
auto neg_one_f = ov::op::v0::Constant::create(data_node->get_element_type(), ov::Shape{}, {-1.0f});
auto paired_shape = ov::op::v0::Constant::create(
ov::element::i64, {5}, std::vector<int64_t>{batch, -1, n_heads, half, 2});
auto x_paired = std::make_shared<ov::op::v1::Reshape>(rot_data, paired_shape, false);
auto split_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {-1});
auto data_split = std::make_shared<ov::op::v1::Split>(x_paired, split_axis, 2);
Output<Node> x0 = data_split->outputs()[0];
Output<Node> x1 = data_split->outputs()[1];
auto x1_neg = std::make_shared<ov::op::v1::Multiply>(x1, neg_one_f);
auto x_rotated_paired = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{x1_neg, x0}, -1);
auto flat_shape =
ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{batch, -1, n_heads, n_dims});
auto x_rotated =
std::make_shared<ov::op::v1::Reshape>(x_rotated_paired, flat_shape, false);
// Expand cos/sin from [..., n_dims/2] to [..., n_dims] by repeating each
// entry twice. Use special_zero on the final Reshape so the seq dim passes
// through dynamically. Final rank is 4 to satisfy the matcher's predicate.
auto expand_cos_sin = [&](const Output<Node>& cs) {
auto cs_unsq = std::make_shared<ov::op::v0::Unsqueeze>(
cs, ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}));
auto bcast_target = ov::op::v0::Constant::create(
ov::element::i64, {5}, std::vector<int64_t>{1, 1, 1, half, 2});
auto bcast = std::make_shared<ov::op::v3::Broadcast>(
cs_unsq, bcast_target, ov::op::BroadcastType::BIDIRECTIONAL);
auto flat = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{0, 0, 0, n_dims});
return std::make_shared<ov::op::v1::Reshape>(bcast, flat, true);
};
Output<Node> cos_full = expand_cos_sin(cos_theta_node);
Output<Node> sin_full = expand_cos_sin(sin_theta_node);
auto y1 = std::make_shared<ov::op::v1::Multiply>(rot_data, cos_full);
auto y2 = std::make_shared<ov::op::v1::Multiply>(x_rotated, sin_full);
auto rotated = std::make_shared<ov::op::v1::Add>(y1, y2);
ov::OutputVector concat_parts;
if (n_offs > 0) {
auto head_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto head_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs});
auto head = std::make_shared<ov::op::v8::Slice>(data_node, head_start, head_end, step_one, axis_last);
concat_parts.push_back(head);
}
concat_parts.push_back(rotated);
if (n_offs + n_dims < head_dim) {
auto tail_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs + n_dims});
auto tail_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {head_dim});
auto tail = std::make_shared<ov::op::v8::Slice>(data_node, tail_start, tail_end, step_one, axis_last);
concat_parts.push_back(tail);
}
if (concat_parts.size() == 1) {
res = rotated;
} else {
res = std::make_shared<ov::op::v0::Concat>(concat_parts, -1);
}
} else if (mode == TYPE_NEOX || mode == TYPE_IMROPE || mode == TYPE_MROPE || mode == TYPE_VISION) {
const int64_t half = (mode == TYPE_VISION) ? n_dims : (n_dims / 2);
const int64_t rot_dims = 2 * half;
if (mode != TYPE_NEOX) {
auto cos_sin_shape = std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{4},
std::vector<int64_t>{1, -1, 1, half});
cos_theta_node = std::make_shared<ov::op::v1::Reshape>(cos_theta_node, cos_sin_shape, true);
sin_theta_node = std::make_shared<ov::op::v1::Reshape>(sin_theta_node, cos_sin_shape, true);
}
auto axis_last = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto step_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
Output<Node> rot_data = data_node;
if (n_offs > 0 || n_offs + rot_dims < head_dim) {
auto rot_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs});
auto rot_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs + rot_dims});
rot_data = std::make_shared<ov::op::v8::Slice>(data_node, rot_start, rot_end, step_one, axis_last);
}
auto neg_one_f = ov::op::v0::Constant::create(data_node->get_element_type(), ov::Shape{}, {-1.0f});
auto split_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {3});
auto split_lengths = ov::op::v0::Constant::create(ov::element::i64, {2}, {half, half});
auto data_split = std::make_shared<ov::op::v1::VariadicSplit>(rot_data, split_axis, split_lengths);
Output<Node> x1 = data_split->outputs()[0];
Output<Node> x2 = data_split->outputs()[1];
auto x2_neg = std::make_shared<ov::op::v1::Multiply>(x2, neg_one_f);
auto x_rotate_half = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{x2_neg, x1}, -1);
auto cos_full = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{cos_theta_node, cos_theta_node}, -1);
auto sin_full = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{sin_theta_node, sin_theta_node}, -1);
auto y1 = std::make_shared<ov::op::v1::Multiply>(rot_data, cos_full);
auto y2 = std::make_shared<ov::op::v1::Multiply>(x_rotate_half, sin_full);
auto rotated = std::make_shared<ov::op::v1::Add>(y1, y2);
ov::OutputVector concat_parts;
if (n_offs > 0) {
auto head_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto head_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs});
auto head = std::make_shared<ov::op::v8::Slice>(data_node, head_start, head_end, step_one, axis_last);
concat_parts.push_back(head);
}
concat_parts.push_back(rotated);
if (n_offs + rot_dims < head_dim) {
auto tail_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs + rot_dims});
auto tail_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {head_dim});
auto tail = std::make_shared<ov::op::v8::Slice>(data_node, tail_start, tail_end, step_one, axis_last);
concat_parts.push_back(tail);
}
if (concat_parts.size() == 1) {
res = rotated;
} else {
res = std::make_shared<ov::op::v0::Concat>(concat_parts, -1);
}
}
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,90 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/core/except.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/equal.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/greater_eq.hpp>
#include <openvino/op/if.hpp>
#include <openvino/op/less.hpp>
#include <openvino/op/logical_or.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/range.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_scale(const NodeContext & context) {
num_inputs_check(context, 1, 1);
float scale;
float bias;
memcpy(&scale, (float *) context.get_output_op_params() + 0, sizeof(float));
memcpy(&bias, (float *) context.get_output_op_params() + 1, sizeof(float));
auto scale_node = std::make_shared<ov::op::v0::Constant>(ov::element::f32, ov::Shape{}, std::vector<float>{scale});
if (context.get_op_case() == 2) {
return {context.get_input(0)};
}
if (context.get_op_case() == 1 && context.has_input("cache_rs_reset_len")) {
auto cache_rs_reset_idx = context.get_input("cache_rs_reset_idx");
auto cache_rs_reset_len = context.get_input("cache_rs_reset_len");
auto cache_rs = context.get_input(0);
auto cache_shape = std::make_shared<ov::op::v3::ShapeOf>(cache_rs, ov::element::i64);
auto n_slots_1d = std::make_shared<ov::op::v8::Gather>(
cache_shape, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}),
ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}));
auto n_slots = std::make_shared<ov::op::v0::Squeeze>(n_slots_1d);
auto iota = std::make_shared<ov::op::v4::Range>(
ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}), n_slots,
ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {1}), ov::element::i64);
auto idx_plus_len = std::make_shared<ov::op::v1::Add>(cache_rs_reset_idx, cache_rs_reset_len);
auto less_than_idx = std::make_shared<ov::op::v1::Less>(iota, cache_rs_reset_idx);
auto greater_equal_idx_plus_len = std::make_shared<ov::op::v1::GreaterEqual>(iota, idx_plus_len);
auto keep_mask = std::make_shared<ov::op::v1::LogicalOr>(less_than_idx, greater_equal_idx_plus_len);
auto keep_mask_f32 = std::make_shared<ov::op::v0::Convert>(keep_mask, ov::element::f32);
auto keep_mask_reshape = std::make_shared<ov::op::v0::Unsqueeze>(
keep_mask_f32, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}));
auto cleared_cache_rs = std::make_shared<ov::op::v1::Multiply>(cache_rs, keep_mask_reshape);
return rename_outputs_with_suffix({cleared_cache_rs}, context.get_name());
}
auto scaled = std::make_shared<ov::op::v1::Multiply>(context.get_input(0), scale_node);
std::shared_ptr<ov::Node> res;
if (bias != 0.0f) {
auto bias_node =
std::make_shared<ov::op::v0::Constant>(ov::element::f32, ov::Shape{}, std::vector<float>{bias});
res = std::make_shared<ov::op::v1::Add>(scaled, bias_node);
} else {
res = scaled;
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,76 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <cstdint>
#include <openvino/frontend/exception.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/range.hpp>
#include <openvino/op/reduce_prod.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/scatter_update.hpp>
#include <openvino/op/shape_of.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML SET writes src1 into a view of src0 and returns the updated tensor.
OutputVector translate_set(const NodeContext & context) {
num_inputs_check(context, 2, 2);
auto dst = process_view_input_new(context, 0);
auto src = process_view_input_new(context, 1);
src = std::make_shared<ov::op::v0::Convert>(src, context.get_output_type());
const auto dst_stride = context.get_input_stride(0);
FRONT_END_OP_CONVERSION_CHECK(dst_stride.size() >= 4, "SET requires 4D destination strides");
const auto * op_params = reinterpret_cast<const uint32_t *>(context.get_output_op_params());
const size_t offset = static_cast<size_t>(op_params[3]);
const size_t elem_size = dst_stride.back();
FRONT_END_OP_CONVERSION_CHECK(elem_size != 0 && offset % elem_size == 0,
"SET offset must be aligned to destination element size");
const int64_t offset_elems = static_cast<int64_t>(offset / elem_size);
auto dst_flat = std::make_shared<ov::op::v1::Reshape>(
dst,
ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}),
false);
auto src_flat = std::make_shared<ov::op::v1::Reshape>(
src,
ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}),
false);
auto src_shape = std::make_shared<ov::op::v3::ShapeOf>(src_flat, ov::element::i64);
auto src_len = std::make_shared<ov::op::v1::ReduceProd>(
src_shape,
ov::op::v0::Constant::create(ov::element::i64, {1}, {0}),
false);
auto start = ov::op::v0::Constant::create(ov::element::i64, {}, {offset_elems});
auto stop = std::make_shared<ov::op::v1::Add>(start, src_len);
auto step = ov::op::v0::Constant::create(ov::element::i64, {}, {1});
auto indices = std::make_shared<ov::op::v4::Range>(start, stop, step, ov::element::i64);
auto axis = ov::op::v0::Constant::create(ov::element::i64, {}, {0});
auto updated_flat = std::make_shared<ov::op::v3::ScatterUpdate>(dst_flat, indices, src_flat, axis);
auto dst_shape = std::make_shared<ov::op::v3::ShapeOf>(dst, ov::element::i64);
auto res = std::make_shared<ov::op::v1::Reshape>(updated_flat, dst_shape, false);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,96 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <cassert>
#include <cstdint>
#include <memory>
#include <openvino/core/node.hpp>
#include <openvino/core/node_output.hpp>
#include <openvino/core/type.hpp>
#include <openvino/frontend/exception.hpp>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/scatter_elements_update.hpp>
#include <openvino/op/scatter_update.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/transpose.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_set_rows(const NodeContext & context) {
num_inputs_check(context, 3, 3);
auto data = process_view_input_new(context, 0);
auto indices = process_view_input_new(context, 1);
auto dst = process_view_input_new(context, 2);
data = std::make_shared<ov::op::v0::Convert>(data, context.get_output_type());
const auto indices_shape = context.get_input_shape(1);
const bool multidim_indices = indices_shape.rank().is_static() &&
indices_shape.rank().get_length() == 4 &&
((indices_shape[1].is_static() && indices_shape[1].get_length() > 1) ||
(indices_shape[2].is_static() && indices_shape[2].get_length() > 1));
auto axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {2});
Output<Node> res;
if (context.is_stateful()) {
int concat_axis = 1;
int64_t dim2 = dst.get_partial_shape()[2].get_length();
int64_t dim3 = dst.get_partial_shape()[3].get_length();
data = std::make_shared<ov::op::v1::Reshape>(
data, ov::op::v0::Constant::create(ov::element::i64, {4}, {(int64_t) 1, (int64_t) -1, dim2, dim3}), false);
res = std::make_shared<ov::op::v0::Concat>(OutputVector{dst, data}, concat_axis);
} else if (multidim_indices) {
auto updates_shape = std::make_shared<ov::op::v3::ShapeOf>(data, ov::element::i64);
auto indices_rank3 = std::make_shared<ov::op::v0::Squeeze>(
indices, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto indices_rank4_shape = std::make_shared<ov::op::v0::Concat>(OutputVector{get_dimensions(updates_shape, {0, 1, 2}), one}, 0);
auto indices_rank4 = std::make_shared<ov::op::v1::Reshape>(indices_rank3, indices_rank4_shape, false);
auto broadcasted_indices = std::make_shared<ov::op::v3::Broadcast>(indices_rank4, updates_shape);
res = std::make_shared<ov::op::v3::ScatterElementsUpdate>(dst, broadcasted_indices, data, axes);
} else {
auto row_size = context.get_input_shape(2)[3].get_length();
auto ind_squeezed = std::make_shared<ov::op::v0::Squeeze>(
indices, ov::op::v0::Constant::create(ov::element::i64, {3}, {0, 1, 2}));
auto data_reshaped = std::make_shared<ov::op::v1::Reshape>(
data,
ov::op::v0::Constant::create(ov::element::i64, {4},
{(int64_t) 1, (int64_t) 1, (int64_t) -1, (int64_t) row_size}),
false);
res = std::make_shared<ov::op::v3::ScatterUpdate>(dst, ind_squeezed, data_reshaped, axes);
}
auto dst_reshape = ov::as_type_ptr<ov::op::v1::Reshape>(dst.get_node_shared_ptr());
if (!multidim_indices && dst_reshape) {
// Fix the case of multiple sequences, reshape back to original shape [1, n_seq, ctx_per_seq, emb]
// ctx_per_seq is not fixed due to llama-bench compatibility
auto dst_shape_partial = dst_reshape->get_input_partial_shape(0);
std::vector<int64_t> dst_shape = {dst_shape_partial[0].get_length(), dst_shape_partial[1].get_length(),
dst_shape_partial[2].is_static() ? dst_shape_partial[2].get_length() : -1,
dst_shape_partial[3].get_length()};
res = std::make_shared<ov::op::v1::Reshape>(res, ov::op::v0::Constant::create(ov::element::i64, {4}, dst_shape),
false);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,166 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <cmath>
#include <cstdint>
#include <cstring>
#include <memory>
#include <openvino/op/broadcast.hpp>
#include <openvino/frontend/exception.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/softmax.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
static bool is_static_one(const ov::Dimension & dim) {
return dim.is_static() && dim.get_length() == 1;
}
static bool same_static_dim(const ov::Dimension & lhs, const ov::Dimension & rhs) {
return lhs.is_static() && rhs.is_static() && lhs.get_length() == rhs.get_length();
}
static bool is_attention_sinks_input_shape(const ov::PartialShape & candidate, const ov::PartialShape & logits_shape) {
if (candidate.rank().is_dynamic() || logits_shape.rank().is_dynamic() || candidate.rank().get_length() != 4 ||
logits_shape.rank().get_length() != 4) {
return false;
}
return is_static_one(candidate[0]) && is_static_one(candidate[1]) && is_static_one(candidate[2]) &&
same_static_dim(candidate[3], logits_shape[1]);
}
// Reimplementation of GGML_OP_SOFT_MAX semantics for OpenVINO backend:
// 1) logits = src0 * scale
// 2) logits += mask (if provided)
// 3) append attention sinks as hidden logits (if provided)
// 4) softmax over the last dimension and remove the hidden sink column
OutputVector translate_soft_max(const NodeContext & context) {
num_inputs_check(context, 1, 3);
float scale = 1.0f;
float max_bias = 0.0f;
memcpy(&scale, (float *) context.get_output_op_params() + 0, sizeof(float));
memcpy(&max_bias, (float *) context.get_output_op_params() + 1, sizeof(float));
ov::Output<ov::Node> logits = context.get_input(0);
const bool second_input_is_sinks =
context.get_input_size() == 2 && is_attention_sinks_input_shape(context.get_input_shape(1), context.get_output_shape());
const bool has_mask = context.get_input_size() > 1 && !second_input_is_sinks;
const bool has_sinks = second_input_is_sinks || context.get_input_size() > 2;
const size_t sinks_input_idx = second_input_is_sinks ? 1 : 2;
// Apply scale first: logits = src0 * scale
if (scale != 1.0f) {
auto scale_const =
std::make_shared<ov::op::v0::Constant>(ov::element::f32, ov::Shape{}, std::vector<float>{scale});
logits = std::make_shared<ov::op::v1::Multiply>(logits, scale_const);
}
FRONT_END_CHECK_IMPLEMENTED(!(max_bias > 0.0f && !has_mask),
"OpenVINO softmax ALiBi path requires mask input");
// Optional mask add: logits += mask
// For max_bias > 0 (ALiBi), apply per-head slope to mask before adding.
if (has_mask) {
ov::Output<ov::Node> mask = context.get_input(1);
// For stateful
std::string mask_name = "KQ_mask_sliced";
if (context.get_input_names()[1].find("swa") != std::string::npos) {
mask_name = "KQ_mask_swa_sliced";
}
if (context.has_input(mask_name)) {
mask = context.get_input(mask_name);
}
if (mask.get_element_type() != logits.get_element_type()) {
mask = std::make_shared<ov::op::v0::Convert>(mask, logits.get_element_type());
}
if (max_bias > 0.0f) {
auto out_shape = context.get_output_shape().to_shape();
FRONT_END_CHECK_IMPLEMENTED(out_shape.size() == 4, "OpenVINO softmax ALiBi path expects rank-4 tensor");
const uint32_t n_head = static_cast<uint32_t>(out_shape[1]);
FRONT_END_CHECK_IMPLEMENTED(n_head > 0, "OpenVINO softmax ALiBi path expects n_head > 0");
const uint32_t n_head_log2 = 1u << static_cast<uint32_t>(std::floor(std::log2(static_cast<float>(n_head))));
const float m0 = std::pow(2.0f, -(max_bias) / static_cast<float>(n_head_log2));
const float m1 = std::pow(2.0f, -(max_bias / 2.0f) / static_cast<float>(n_head_log2));
std::vector<float> slopes(n_head);
for (uint32_t h = 0; h < n_head; ++h) {
slopes[h] = h < n_head_log2 ? std::pow(m0, static_cast<float>(h + 1)) :
std::pow(m1, static_cast<float>(2 * (h - n_head_log2) + 1));
}
ov::Output<ov::Node> slope_node =
std::make_shared<ov::op::v0::Constant>(ov::element::f32, ov::Shape{n_head}, slopes);
if (slope_node.get_element_type() != mask.get_element_type()) {
slope_node = std::make_shared<ov::op::v0::Convert>(slope_node, mask.get_element_type());
}
auto slope_shape = std::make_shared<ov::op::v0::Constant>(
ov::element::i64, ov::Shape{4}, std::vector<int64_t>{1, static_cast<int64_t>(n_head), 1, 1});
auto slope_4d = std::make_shared<ov::op::v1::Reshape>(slope_node, slope_shape, false);
mask = std::make_shared<ov::op::v1::Multiply>(mask, slope_4d);
}
logits = std::make_shared<ov::op::v1::Add>(logits, mask);
}
ov::Output<ov::Node> softmax_input = logits;
if (has_sinks) {
ov::Output<ov::Node> sinks = context.get_input(sinks_input_idx);
if (sinks.get_element_type() != logits.get_element_type()) {
sinks = std::make_shared<ov::op::v0::Convert>(sinks, logits.get_element_type());
}
auto sink_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, {1, -1, 1, 1});
auto sinks_4d = std::make_shared<ov::op::v1::Reshape>(sinks, sink_shape, false);
auto logits_shape = std::make_shared<ov::op::v3::ShapeOf>(logits, ov::element::i64);
auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto three = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
auto four = ov::op::v0::Constant::create(ov::element::i64, {1}, {4});
auto shape_axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto sink_prefix_shape = std::make_shared<ov::op::v8::Slice>(logits_shape, zero, three, one, shape_axis);
auto sink_last_dim = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto sink_broadcast_shape = std::make_shared<ov::op::v0::Concat>(
ov::OutputVector{sink_prefix_shape, sink_last_dim}, 0);
auto sink_column = std::make_shared<ov::op::v3::Broadcast>(sinks_4d, sink_broadcast_shape,
ov::op::BroadcastType::BIDIRECTIONAL);
softmax_input = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{logits, sink_column}, 3);
auto softmax_with_sink = std::make_shared<ov::op::v8::Softmax>(softmax_input, -1);
auto original_last_dim = std::make_shared<ov::op::v8::Slice>(logits_shape, three, four, one, shape_axis);
auto res = std::make_shared<ov::op::v8::Slice>(softmax_with_sink, zero, original_last_dim, one, three);
return rename_outputs_with_suffix({res}, context.get_name());
}
// Softmax along last dimension (equivalent to ggml softmax over ne[0]).
auto res = std::make_shared<ov::op::v8::Softmax>(softmax_input, -1);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,108 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/broadcast.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/loop.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/scatter_update.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/subtract.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML SOLVE_TRI: solve Ax = B for lower-triangular A via forward substitution.
// Currently only lower, right, non-unitriangular variant is implemented.
//
// ggml layout: A [n, n, B1, B2], B [k, n, B1, B2] → X [k, n, B1, B2]
// OV layout: A [B2, B1, n, n], B [B2, B1, n, k] → X [B2, B1, n, k]
//
// Forward substitution row i:
// x[i] = (b[i] - sum_{t<i} A[i,t]*x[t]) / A[i,i]
//
// Implemented as an OV Loop op iterating n times with a carried X accumulator.
// Key insight: A is lower-triangular and X starts as zeros, so the full matmul
// A_row_i @ X_partial = sum_{t<i} A[i,t]*x[t] exactly (upper triangle of A
// is zero; unfilled rows of X are zero).
OutputVector translate_solve_tri(const NodeContext & context) {
num_inputs_check(context, 2, 2);
auto A = context.get_input(0); // [B2, B1, n, n]
auto B = context.get_input(1); // [B2, B1, n, k]
auto A_shape = context.get_input_shape(0).to_shape();
int64_t n = static_cast<int64_t>(A_shape[2]);
// Initial X: zeros with shape of B
auto B_shape_node = std::make_shared<ov::op::v3::ShapeOf>(B, ov::element::i64);
auto zero_f32 = ov::op::v0::Constant::create(ov::element::f32, {}, {0.0f});
auto X_init = std::make_shared<ov::op::v3::Broadcast>(zero_f32, B_shape_node);
// --- Loop body parameters ---
// body_iter: iteration counter injected by the Loop op (i64, shape {1})
auto body_iter = std::make_shared<ov::op::v0::Parameter>(ov::element::i64, ov::Shape{1});
auto body_X = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape::dynamic(4));
auto body_A = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape::dynamic(4));
auto body_B_p = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape::dynamic(4));
auto c_axis2 = ov::op::v0::Constant::create(ov::element::i64, {1}, {int64_t(2)});
auto c_axis3 = ov::op::v0::Constant::create(ov::element::i64, {1}, {int64_t(3)});
auto c_axis2_scalar = ov::op::v0::Constant::create(ov::element::i64, {}, {int64_t(2)});
// b_i = B[..., i, :] [B2, B1, 1, k]
auto b_i = std::make_shared<ov::op::v8::Gather>(body_B_p, body_iter, c_axis2);
// A_row_i = A[..., i, :] [B2, B1, 1, n]
auto A_row_i = std::make_shared<ov::op::v8::Gather>(body_A, body_iter, c_axis2);
// sum_i = A_row_i @ X [B2, B1, 1, k]
// (lower-tri zeros + unfilled-X zeros make this equal to the partial sum)
auto sum_i = std::make_shared<ov::op::v0::MatMul>(A_row_i, body_X, false, false);
// diag_i = A[..., i, i] [B2, B1, 1, 1]
auto diag_i = std::make_shared<ov::op::v8::Gather>(A_row_i, body_iter, c_axis3);
// x_i = (b_i - sum_i) / diag_i [B2, B1, 1, k]
auto x_i = std::make_shared<ov::op::v1::Divide>(
std::make_shared<ov::op::v1::Subtract>(b_i, sum_i), diag_i);
// X_updated: scatter x_i into body_X at row i along axis 2
auto X_updated = std::make_shared<ov::op::v3::ScatterUpdate>(body_X, body_iter, x_i, c_axis2_scalar);
auto body_cond = ov::op::v0::Constant::create(ov::element::boolean, ov::Shape{1}, {true});
auto body = std::make_shared<ov::Model>(
ov::OutputVector{body_cond, X_updated},
ov::ParameterVector{body_iter, body_X, body_A, body_B_p});
// --- Assemble Loop ---
auto trip_count = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector<int64_t>{n});
auto exec_cond = ov::op::v0::Constant::create(ov::element::boolean, ov::Shape{1}, {true});
auto loop = std::make_shared<ov::op::v5::Loop>(trip_count, exec_cond);
loop->set_function(body);
// iter_counter_body_param_idx=0 (body_iter), exec_condition_body_result_idx=0 (body_cond)
loop->set_special_body_ports(ov::op::v5::Loop::SpecialBodyPorts{0, 0});
// Carried state: X feeds back from X_updated each iteration
loop->set_merged_input(body_X, X_init, X_updated);
// Invariant inputs passed through unchanged
loop->set_invariant_input(body_A, A);
loop->set_invariant_input(body_B_p, B);
// Final output: value of X_updated after the last iteration
auto X_final = loop->get_iter_value(X_updated, -1);
return rename_outputs_with_suffix({X_final}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,35 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/op/multiply.hpp>
#include <openvino/op/sqrt.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_sqr(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto res = std::make_shared<ov::op::v1::Multiply>(input, input);
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_sqrt(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto res = std::make_shared<ov::op::v0::Sqrt>(input);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,61 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/constant.hpp>
#include <openvino/op/group_conv.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/unsqueeze.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_ssm_conv(const NodeContext & context) {
num_inputs_check(context, 2, 2);
auto sx = context.get_input(0); // conv state + input: OV shape [1, n_s, d_inner, ncs]
auto c = context.get_input(1); // conv1d weight: OV shape [1, 1, d_inner, d_conv]
auto sx_shape = context.get_input_shape(0).to_shape(); // [1, n_s, d_inner, ncs]
auto c_shape = context.get_input_shape(1).to_shape(); // [1, 1, d_inner, d_conv]
// int64_t n_s = sx_shape[1];
int64_t d_inner = sx_shape[2];
// int64_t ncs = sx_shape[3]; // d_conv - 1 + n_t
int64_t d_conv = c_shape[3];
// int64_t n_t = ncs - d_conv + 1;
// Reshape sx from [1, n_s, d_inner, ncs] to [n_s, d_inner, ncs] for 1D GroupConvolution
auto sx_reshaped =
std::make_shared<ov::op::v0::Squeeze>(sx, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
// Reshape c from [1, 1, d_inner, d_conv] to [d_inner, 1, 1, d_conv]
// GroupConvolution filter: [groups, out_channels/groups, in_channels/groups, kernel_size]
auto c_new_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{d_inner, 1, 1, d_conv});
auto c_reshaped = std::make_shared<ov::op::v1::Reshape>(c, c_new_shape, false);
// Depthwise 1D convolution: groups=d_inner, stride=1, no padding, no dilation
// Input: [n_s, d_inner, ncs], Filter: [d_inner, 1, 1, d_conv]
// Output: [n_s, d_inner, n_t]
auto conv = std::make_shared<ov::op::v1::GroupConvolution>(
sx_reshaped, c_reshaped, ov::Strides{1}, ov::CoordinateDiff{0}, ov::CoordinateDiff{0}, ov::Strides{1});
// Transpose from [n_s, d_inner, n_t] to [n_s, n_t, d_inner]
auto perm = ov::op::v0::Constant::create(ov::element::i64, {3}, std::vector<int64_t>{0, 2, 1});
auto transposed = std::make_shared<ov::op::v1::Transpose>(conv, perm);
// Reshape to output shape [1, n_s, n_t, d_inner]
auto res =
std::make_shared<ov::op::v0::Unsqueeze>(transposed, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,30 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/op/constant.hpp>
#include <openvino/op/range.hpp>
#include <openvino/op/reduce_sum.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_sum(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 1, 2, 3});
auto res = std::make_shared<ov::op::v1::ReduceSum>(input, axes, true);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,27 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/op/constant.hpp>
#include <openvino/op/reduce_sum.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_sum_rows(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto res = std::make_shared<ov::op::v1::ReduceSum>(
input, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}), true);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,52 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/transpose.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_transpose(const NodeContext & context) {
num_inputs_check(context, 1, 1);
// Compute permute order from input/output shape and stride information
// so it adapts to different input and output layouts.
auto input_stride = context.get_input_stride(0);
auto output_stride = context.get_output_stride();
// Compute permute order by matching output and input stride rankings.
// Build <stride, dim_index> pairs.
std::vector<std::pair<size_t, int>> output_stride_dims;
std::vector<std::pair<size_t, int>> input_stride_dims;
for (int i = 0; i < 4; ++i) {
output_stride_dims.push_back({output_stride[i], i});
input_stride_dims.push_back({input_stride[i], i});
}
// Sort by stride in descending order.
std::sort(output_stride_dims.rbegin(), output_stride_dims.rend());
std::sort(input_stride_dims.rbegin(), input_stride_dims.rend());
// Build permute order.
std::vector<int64_t> permute_order(4);
for (int i = 0; i < 4; ++i) {
int output_dim = output_stride_dims[i].second;
int input_dim = input_stride_dims[i].second;
permute_order[output_dim] = input_dim;
}
auto input = process_view_input_new(context, 0);
auto res = std::make_shared<ov::op::v1::Transpose>(
input, ov::op::v0::Constant::create(ov::element::i64, {4}, permute_order));
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,82 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/constant.hpp>
#include <openvino/op/greater.hpp>
#include <openvino/op/greater_eq.hpp>
#include <openvino/op/less.hpp>
#include <openvino/op/less_eq.hpp>
#include <openvino/op/range.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/select.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML TRI zeroes out elements outside a triangular region of a square matrix.
// The type param (stored in op_params[0]) maps to ggml_tri_type:
// 0 = UPPER_DIAG : keep where col >= row
// 1 = UPPER : keep where col > row
// 2 = LOWER_DIAG : keep where col <= row
// 3 = LOWER : keep where col < row
//
// In OV layout (ggml [ne0, ne1, ne2, ne3] → OV [ne3, ne2, ne1, ne0]):
// ggml dim 0 (ne0, cols) → OV axis 3
// ggml dim 1 (ne1, rows) → OV axis 2
// The matrix is square so ne0 == ne1.
OutputVector translate_tri(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto x = context.get_input(0); // OV shape: [ne3, ne2, ne1, ne0]
int32_t tri_type = context.get_output_op_params()[0];
auto shape = context.get_input_shape(0).to_shape();
int64_t n = static_cast<int64_t>(shape[3]); // ne0 == ne1
// Build index range [0, 1, ..., n-1]
auto start = ov::op::v0::Constant::create(ov::element::i64, {}, {int64_t(0)});
auto stop = ov::op::v0::Constant::create(ov::element::i64, {}, {n});
auto step = ov::op::v0::Constant::create(ov::element::i64, {}, {int64_t(1)});
auto range = std::make_shared<ov::op::v4::Range>(start, stop, step, ov::element::i64);
// col_idx shape [1, 1, 1, n] — broadcasts over batch and row dims
auto col_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, 1, n});
auto col_idx = std::make_shared<ov::op::v1::Reshape>(range, col_shape, false);
// row_idx shape [1, 1, n, 1] — broadcasts over batch and col dims
auto row_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, n, 1});
auto row_idx = std::make_shared<ov::op::v1::Reshape>(range, row_shape, false);
// Build boolean mask: true where element should be kept
std::shared_ptr<ov::Node> mask;
switch (tri_type) {
case 0: // UPPER_DIAG: col >= row
mask = std::make_shared<ov::op::v1::GreaterEqual>(col_idx, row_idx);
break;
case 1: // UPPER: col > row
mask = std::make_shared<ov::op::v1::Greater>(col_idx, row_idx);
break;
case 2: // LOWER_DIAG: col <= row
mask = std::make_shared<ov::op::v1::LessEqual>(col_idx, row_idx);
break;
case 3: // LOWER: col < row
mask = std::make_shared<ov::op::v1::Less>(col_idx, row_idx);
break;
default:
throw std::runtime_error("translate_tri: invalid tri_type " + std::to_string(tri_type));
}
auto zero = ov::op::v0::Constant::create(ov::element::f32, {}, {0.0f});
auto res = std::make_shared<ov::op::v1::Select>(mask, x, zero);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,138 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include "ggml-openvino/ggml-openvino-extra.h"
#include <memory>
#include <openvino/op/abs.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/clamp.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/elu.hpp>
#include <openvino/op/exp.hpp>
#include <openvino/op/gelu.hpp>
#include <openvino/op/greater.hpp>
#include <openvino/op/hard_sigmoid.hpp>
#include <openvino/op/log.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/negative.hpp>
#include <openvino/op/relu.hpp>
#include <openvino/op/round.hpp>
#include <openvino/op/sigmoid.hpp>
#include <openvino/op/softplus.hpp>
#include <openvino/op/subtract.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_unary_gelu(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto res = std::make_shared<ov::op::v7::Gelu>(input, ov::op::GeluApproximationMode::TANH);
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_unary_gelu_erf(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto res = std::make_shared<ov::op::v7::Gelu>(input, ov::op::GeluApproximationMode::ERF);
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_unary_gelu_quick(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto scale = ov::op::v0::Constant::create(input.get_element_type(), ov::Shape{}, {1.702f});
auto mul = std::make_shared<ov::op::v1::Multiply>(input, scale);
auto sig = std::make_shared<ov::op::v0::Sigmoid>(mul);
auto res = std::make_shared<ov::op::v1::Multiply>(input, sig);
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_unary_elu(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto res = std::make_shared<ov::op::v0::Elu>(input, 1.0);
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_unary_hardsigmoid(const NodeContext & context) {
num_inputs_check(context, 1, 1);
// compute in f32 like the ggml reference: 1/6 is not exact in f16/bf16 (NPU cannot take the f32 path)
auto input = process_view_input_new(context, 0);
const auto type = ggml_openvino_is_npu() ? input.get_element_type() : ov::element::f32;
ov::Output<ov::Node> x = input;
if (type != input.get_element_type()) {
x = std::make_shared<ov::op::v0::Convert>(input, type);
}
auto alpha = ov::op::v0::Constant::create(type, ov::Shape{}, {1.0f / 6.0f});
auto beta = ov::op::v0::Constant::create(type, ov::Shape{}, {0.5f});
ov::Output<ov::Node> res = std::make_shared<ov::op::v0::HardSigmoid>(x, alpha, beta);
if (type != input.get_element_type()) {
res = std::make_shared<ov::op::v0::Convert>(res, input.get_element_type());
}
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_unary_step(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto zero = ov::op::v0::Constant::create(input.get_element_type(), ov::Shape{}, {0.0f});
auto cond = std::make_shared<ov::op::v1::Greater>(input, zero);
auto res = std::make_shared<ov::op::v0::Convert>(cond, input.get_element_type());
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_unary_round(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto res = std::make_shared<ov::op::v5::Round>(input, ov::op::v5::Round::RoundMode::HALF_AWAY_FROM_ZERO);
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_unary_expm1(const NodeContext & context) {
num_inputs_check(context, 1, 1);
// compute in f32 like the ggml reference: exp(x) - 1 in f16 loses the small-x digits (NPU cannot take the f32 path)
auto input = process_view_input_new(context, 0);
const auto type = ggml_openvino_is_npu() ? input.get_element_type() : ov::element::f32;
ov::Output<ov::Node> x = input;
if (type != input.get_element_type()) {
x = std::make_shared<ov::op::v0::Convert>(input, type);
}
auto exp = std::make_shared<ov::op::v0::Exp>(x);
auto one = ov::op::v0::Constant::create(type, ov::Shape{}, {1.0f});
ov::Output<ov::Node> res = std::make_shared<ov::op::v1::Subtract>(exp, one);
if (type != input.get_element_type()) {
res = std::make_shared<ov::op::v0::Convert>(res, input.get_element_type());
}
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_unary_softplus(const NodeContext & context) {
num_inputs_check(context, 1, 1);
if (ggml_openvino_getenv_int("GGML_OPENVINO_NATIVE_SOFTPLUS") != 0) {
return translate_1to1_match_1_input<ov::op::v4::SoftPlus>(context);
}
auto input = process_view_input_new(context, 0);
const auto element_type = input.get_element_type();
auto one = ov::op::v0::Constant::create(element_type, ov::Shape{}, {1.0f});
auto positive = std::make_shared<ov::op::v0::Relu>(input);
auto abs = std::make_shared<ov::op::v0::Abs>(input);
auto neg_abs = std::make_shared<ov::op::v0::Negative>(abs);
auto exp_neg_abs = std::make_shared<ov::op::v0::Exp>(neg_abs);
auto log_term = std::make_shared<ov::op::v0::Log>(std::make_shared<ov::op::v1::Add>(one, exp_neg_abs));
auto res = std::make_shared<ov::op::v1::Add>(positive, log_term);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,181 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include "ggml.h"
#include <cmath>
#include <cstddef>
#include <memory>
#include <openvino/op/constant.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/interpolate.hpp>
#include <openvino/op/matmul.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_upscale(const NodeContext & context) {
num_inputs_check(context, 1, 1);
using Interpolate = ov::op::v4::Interpolate;
auto input = process_view_input_new(context, 0);
const auto input_shape = context.get_input_shape(0).to_shape();
const auto output_shape = context.get_output_shape().to_shape();
if (input_shape == output_shape) {
return rename_outputs_with_suffix({input}, context.get_name());
}
ov::Output<ov::Node> res = input;
// Resample batch / channel dimensions (ne[3] and ne[2], corresponding to OV axes 0 and 1)
// using nearest-neighbor index mapping: i0_d = floor(i_d * in_d / out_d).
for (size_t axis = 0; axis < 2; ++axis) {
const size_t in_dim = input_shape[axis];
const size_t out_dim = output_shape[axis];
if (in_dim != out_dim) {
std::vector<int64_t> indices(out_dim);
for (size_t i = 0; i < out_dim; ++i) {
indices[i] = static_cast<int64_t>((i * in_dim) / out_dim);
}
auto indices_node = ov::op::v0::Constant::create(ov::element::i64, {indices.size()}, indices);
auto axis_node = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {axis});
res = std::make_shared<ov::op::v8::Gather>(res, indices_node, axis_node);
}
}
// Spatial interpolation for ne[1] and ne[0] (OV axes 2 and 3)
if (input_shape[2] != output_shape[2] || input_shape[3] != output_shape[3]) {
const int32_t * op_params = context.get_output_op_params();
const int32_t mode_flags = op_params != nullptr ? op_params[0] : 0;
const int op_case = context.get_op_case();
const bool align_corners = (mode_flags & GGML_SCALE_FLAG_ALIGN_CORNERS) != 0;
const bool antialias = (mode_flags & GGML_SCALE_FLAG_ANTIALIAS) != 0;
if (op_case == 2 && antialias) { // GGML_SCALE_MODE_BILINEAR with antialias
const size_t H_in = input_shape[2];
const size_t W_in = input_shape[3];
const size_t H_out = output_shape[2];
const size_t W_out = output_shape[3];
const float pixel_offset = 0.5f;
// Height projection: [H_out, H_in]
std::vector<float> Wy_data(H_out * H_in, 0.0f);
const float sf1 = static_cast<float>(H_out) / H_in;
const float support1 = std::max(1.0f, 1.0f / sf1);
const float invscale1 = 1.0f / support1;
for (size_t i1 = 0; i1 < H_out; ++i1) {
const float y = (static_cast<float>(i1) + pixel_offset) / sf1;
const auto y_start = static_cast<int64_t>(y - support1 + pixel_offset);
const size_t y_min = y_start > 0 ? static_cast<size_t>(y_start) : 0;
const auto y_end = static_cast<int64_t>(y + support1 + pixel_offset);
const size_t y_max = y_end > 0 ? std::min<size_t>(static_cast<size_t>(y_end), H_in) : 0;
float total_weight = 0.0f;
for (size_t sy = y_min; sy < y_max; ++sy) {
float diff = std::abs((static_cast<float>(sy) - y + pixel_offset) * invscale1);
float weight = std::max(1.0f - diff, 0.0f);
Wy_data[i1 * H_in + sy] = weight;
total_weight += weight;
}
if (total_weight > 0.0f) {
for (size_t sy = y_min; sy < y_max; ++sy) {
Wy_data[i1 * H_in + sy] /= total_weight;
}
}
}
// Width projection: [W_in, W_out]
std::vector<float> Wx_data(W_in * W_out, 0.0f);
const float sf0 = static_cast<float>(W_out) / W_in;
const float support0 = std::max(1.0f, 1.0f / sf0);
const float invscale0 = 1.0f / support0;
for (size_t i0 = 0; i0 < W_out; ++i0) {
const float x = (static_cast<float>(i0) + pixel_offset) / sf0;
const auto x_start = static_cast<int64_t>(x - support0 + pixel_offset);
const size_t x_min = x_start > 0 ? static_cast<size_t>(x_start) : 0;
const auto x_end = static_cast<int64_t>(x + support0 + pixel_offset);
const size_t x_max = x_end > 0 ? std::min<size_t>(static_cast<size_t>(x_end), W_in) : 0;
float total_weight = 0.0f;
for (size_t sx = x_min; sx < x_max; ++sx) {
float diff = std::abs((static_cast<float>(sx) - x + pixel_offset) * invscale0);
float weight = std::max(1.0f - diff, 0.0f);
Wx_data[sx * W_out + i0] = weight;
total_weight += weight;
}
if (total_weight > 0.0f) {
for (size_t sx = x_min; sx < x_max; ++sx) {
Wx_data[sx * W_out + i0] /= total_weight;
}
}
}
auto Wy_node = ov::op::v0::Constant::create(ov::element::f32, {H_out, H_in}, Wy_data);
auto Wx_node = ov::op::v0::Constant::create(ov::element::f32, {W_in, W_out}, Wx_data);
auto res_y = std::make_shared<ov::op::v0::MatMul>(Wy_node, res);
res = std::make_shared<ov::op::v0::MatMul>(res_y, Wx_node);
} else {
Interpolate::InterpolateAttrs attrs;
attrs.shape_calculation_mode = Interpolate::ShapeCalcMode::SIZES;
attrs.antialias = antialias;
attrs.pads_begin = {0, 0, 0, 0};
attrs.pads_end = {0, 0, 0, 0};
switch (op_case) {
case 1: // GGML_SCALE_MODE_NEAREST
attrs.mode = Interpolate::InterpolateMode::NEAREST;
attrs.nearest_mode = Interpolate::NearestMode::FLOOR;
attrs.coordinate_transformation_mode = Interpolate::CoordinateTransformMode::ASYMMETRIC;
break;
case 2: // GGML_SCALE_MODE_BILINEAR
attrs.mode = Interpolate::InterpolateMode::LINEAR;
attrs.coordinate_transformation_mode = align_corners
? Interpolate::CoordinateTransformMode::ALIGN_CORNERS
: Interpolate::CoordinateTransformMode::HALF_PIXEL;
break;
case 3: // GGML_SCALE_MODE_BICUBIC
attrs.mode = Interpolate::InterpolateMode::CUBIC;
attrs.cube_coeff = -0.75;
attrs.coordinate_transformation_mode = align_corners
? Interpolate::CoordinateTransformMode::ALIGN_CORNERS
: Interpolate::CoordinateTransformMode::HALF_PIXEL;
break;
default:
FRONT_END_OP_CONVERSION_CHECK(false, "Unsupported upscale op_case: ", op_case);
}
std::vector<int64_t> target_shape_vec = {
static_cast<int64_t>(output_shape[2]),
static_cast<int64_t>(output_shape[3]),
};
std::vector<float> scales_vec = {
static_cast<float>(output_shape[2]) / static_cast<float>(input_shape[2]),
static_cast<float>(output_shape[3]) / static_cast<float>(input_shape[3]),
};
auto target_shape_node = ov::op::v0::Constant::create(ov::element::i64, {2}, target_shape_vec);
auto scales_node = ov::op::v0::Constant::create(ov::element::f32, {2}, scales_vec);
auto axes_node = ov::op::v0::Constant::create(ov::element::i64, {2}, {2, 3});
res = std::make_shared<Interpolate>(
res, target_shape_node, scales_node, axes_node, attrs);
}
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,259 @@
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_view(const NodeContext & context) {
num_inputs_check(context, 1, 1);
if (context.get_op_case() == 1) {
// Static-mode identity pass-through for VIEWs over a GATED_DELTA_NET combined output or
// the conv_input CONCAT; the consuming op (CPY/RMS_NORM) does its own runtime-correct
// slicing on the full tensor (see ggml-decoder.cpp compute_op_case, GGML_OP_VIEW).
return {context.get_input(0)};
}
if (!context.is_static()) {
// On the stateless/non-static path VIEW is normally a no-op (consumers re-slice).
// EXCEPTION: the MoE expert aggregation slices each expert plane out of
// ffn_moe_weighted [n_embd, n_expert_used, n_tokens] with ggml_view_2d and then
// sums the planes with a chain of ADDs (llama-graph.cpp). Those ADDs read this
// VIEW node directly from the tensor map and do NOT re-slice, so a no-op here
// makes every plane the full tensor and the expert sum collapses. Materialize the
// single-expert slice here. Gated by name (ffn_moe_weighted...view) so it can't
// affect any other view.
const std::string & vname = context.get_name();
if (vname.find("ffn_moe_weighted") != std::string::npos) {
auto src_ps = context.get_input_shape(0);
auto dst_ps = context.get_output_shape();
if (src_ps.rank().is_static() && dst_ps.rank().is_static() && src_ps.rank() == dst_ps.rank() &&
src_ps.is_static() && dst_ps.is_static()) {
auto sst = context.get_input_stride(0);
auto dst = context.get_output_stride();
size_t voff = context.get_output_op_offset();
auto ss = src_ps.to_shape();
auto dd = dst_ps.to_shape();
const size_t nd = ss.size();
// Stateful models drop the leading size-1 batch dim, so the real OV tensor can
// be one rank lower than the ggml shape metadata above. Axis indices derived
// from that metadata must be shifted down by the difference before they are
// used as OV axes. Bail out if the dims we would drop are not all 1.
const auto in_rank = context.get_input(0).get_partial_shape().rank();
const int axis_shift =
in_rank.is_static() ? (int) nd - (int) in_rank.get_length() : 0;
bool shift_ok = axis_shift >= 0 && (size_t) axis_shift < nd;
for (int a = 0; a < axis_shift && shift_ok; ++a) {
shift_ok = (ss[a] == 1 && dd[a] == 1);
}
if (shift_ok && sst.size() == nd && dst.size() == nd) {
// Map each dst axis of size>1 to a src axis with equal (size,stride);
// the unmatched src axis of size>1 is the indexed expert axis.
// dst_to_src[d] records which src axis each dst axis came from, so we can
// later pull the dynamic (token) dim from the right source axis at runtime.
std::vector<bool> used(nd, false);
std::vector<int> dst_to_src(nd, -1);
bool ok = true;
for (size_t d = 0; d < nd; ++d) {
if (dd[d] == 1) {
continue;
}
int found = -1;
for (size_t s = 0; s < nd; ++s) {
if (!used[s] && ss[s] == dd[d] && sst[s] == dst[d]) {
found = (int) s;
break;
}
}
if (found < 0) {
ok = false;
break;
}
used[found] = true;
dst_to_src[d] = found;
}
int dropped = -1;
if (ok) {
for (size_t s = 0; s < nd; ++s) {
if (!used[s] && ss[s] > 1) {
if (dropped >= 0) {
ok = false;
break;
}
dropped = (int) s;
}
}
}
if (ok && dropped >= 0) {
const size_t dstr = sst[dropped];
const int64_t dsz = (int64_t) ss[dropped];
if (dstr > 0 && voff % dstr == 0) {
const int64_t sel = (int64_t) (voff / dstr);
if (sel >= 0 && sel < dsz) {
ov::Output<ov::Node> sl = std::make_shared<ov::op::v8::Slice>(
context.get_input(0),
ov::op::v0::Constant::create(ov::element::i64, {1}, {sel}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {sel + 1}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {1}),
ov::op::v0::Constant::create(ov::element::i64, {1},
{dropped - axis_shift}));
// Build the reshape target from the (concrete) dst shape, but
// keep the dynamic token axis dynamic instead of freezing it
// to the captured n_tokens. Without this the constant dst
// shape bakes in the prefill token count and the static value
// flows downstream, turning every later decoder layer static
// (the GPU in-place-concat KV-cache bug). The token axis is
// PERMUTED between the sliced input and the dst (e.g. input
// [1,tok,expert,emb] -> dst [1,1,tok,emb]), so special_zero
// (which copies the same-position dim) is not enough: pull the
// dynamic dim from the correct SOURCE axis via ShapeOf+Gather
// and place it at the dst token position.
const int32_t dyn = context.get_op_dynamic_dim(); // output ggml axis, -1 if none
// still in ggml metadata axis space; get_shape() reverses ggml order
int dst_ov_axis = (dyn != -1) ? ((int) nd - 1 - (int) dyn) : -1;
int src_ov_axis = (dst_ov_axis >= axis_shift && dst_ov_axis < (int) nd)
? dst_to_src[dst_ov_axis]
: -1;
if (dst_ov_axis >= 0 && src_ov_axis >= axis_shift) {
// target = concat of per-axis scalars; the token axis is a
// runtime Gather of the slice's shape, the rest are constants.
auto sl_shape = std::make_shared<ov::op::v3::ShapeOf>(sl, ov::element::i64);
auto tok_dim = std::make_shared<ov::op::v8::Gather>(
sl_shape,
ov::op::v0::Constant::create(ov::element::i64, {1},
{src_ov_axis - axis_shift}),
ov::op::v0::Constant::create(ov::element::i64, {}, {0}));
ov::OutputVector parts;
for (int a = axis_shift; a < (int) nd; ++a) {
if (a == dst_ov_axis) {
parts.push_back(tok_dim);
} else {
parts.push_back(ov::op::v0::Constant::create(
ov::element::i64, {1}, {(int64_t) dd[a]}));
}
}
auto dc = std::make_shared<ov::op::v0::Concat>(parts, 0);
auto rs = std::make_shared<ov::op::v1::Reshape>(sl, dc, false);
return rename_outputs_with_suffix({rs}, context.get_name());
}
std::vector<int64_t> dd_ov(dd.begin() + axis_shift, dd.end());
auto dc = ov::op::v0::Constant::create(ov::element::i64, {dd_ov.size()}, dd_ov);
auto rs = std::make_shared<ov::op::v1::Reshape>(sl, dc, false);
return rename_outputs_with_suffix({rs}, context.get_name());
}
}
}
}
}
}
return {context.get_input(0)};
}
auto input = context.get_input(0);
auto src_shape = context.get_input_shape(0);
auto dst_shape = context.get_output_shape();
if (src_shape.rank().is_dynamic() || dst_shape.rank().is_dynamic()) {
return {input};
}
int64_t src_elems = 1;
int64_t dst_elems = 1;
for (int64_t i = 0; i < src_shape.rank().get_length(); ++i) {
if (src_shape[i].is_dynamic()) {
return {input};
}
src_elems *= src_shape[i].get_length();
}
for (int64_t i = 0; i < dst_shape.rank().get_length(); ++i) {
if (dst_shape[i].is_dynamic()) {
return {input};
}
dst_elems *= dst_shape[i].get_length();
}
if (dst_elems >= src_elems) {
return {input};
}
auto src_stride = context.get_input_stride(0);
auto dst_stride = context.get_output_stride();
size_t view_offset = context.get_output_op_offset();
bool same_stride = (src_stride.size() == dst_stride.size());
if (same_stride) {
for (size_t i = 0; i < src_stride.size(); ++i) {
if (src_stride[i] != dst_stride[i]) {
same_stride = false;
break;
}
}
}
if (!same_stride) {
return {input};
}
auto src_ov_shape = src_shape.to_shape();
auto dst_ov_shape = dst_shape.to_shape();
size_t ndims = src_ov_shape.size();
if (dst_ov_shape.size() != ndims) {
return {input};
}
std::vector<int> diff_dims;
for (size_t i = 0; i < ndims; ++i) {
if (src_ov_shape[i] != dst_ov_shape[i]) {
diff_dims.push_back(static_cast<int>(i));
}
}
if (diff_dims.size() != 1) {
return {input};
}
int slice_dim = diff_dims[0];
int64_t dim_size = static_cast<int64_t>(src_ov_shape[slice_dim]);
size_t ov_stride_for_dim = 1;
for (size_t i = slice_dim + 1; i < ndims; ++i) {
ov_stride_for_dim *= src_ov_shape[i];
}
size_t elem_size = src_stride.back();
if (elem_size == 0) {
elem_size = 1;
}
int64_t begin_val = 0;
if (ov_stride_for_dim > 0 && elem_size > 0) {
begin_val = static_cast<int64_t>((view_offset / elem_size) / ov_stride_for_dim);
}
int64_t end_val = begin_val + static_cast<int64_t>(dst_ov_shape[slice_dim]);
if (begin_val < 0 || end_val > dim_size) {
return {input};
}
auto sliced =
std::make_shared<ov::op::v8::Slice>(input, ov::op::v0::Constant::create(ov::element::i64, {1}, {begin_val}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {end_val}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {1}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {slice_dim}));
sliced->set_friendly_name(context.get_output_name());
return {sliced->output(0)};
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,122 @@
#include "op_table.h"
#include "utils.h"
#include <openvino/op/abs.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/ceiling.hpp>
#include <openvino/op/cos.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/exp.hpp>
#include <openvino/op/floor.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/gelu.hpp>
#include <openvino/op/hswish.hpp>
#include <openvino/op/log.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/negative.hpp>
#include <openvino/op/relu.hpp>
#include <openvino/op/sigmoid.hpp>
#include <openvino/op/sign.hpp>
#include <openvino/op/sin.hpp>
#include <openvino/op/softplus.hpp>
#include <openvino/op/subtract.hpp>
#include <openvino/op/swish.hpp>
#include <openvino/op/tanh.hpp>
namespace ov {
namespace frontend {
namespace ggml {
std::unordered_map<std::string, CreatorFunction> get_supported_ops() {
using namespace ov::op;
return {
{"GGML_OP_ADD", op::translate_add },
{"GGML_OP_ADD1", op::translate_1to1_match_2_inputs<v1::Add> },
{"GGML_OP_ADD_ID", op::translate_add_id },
{"GGML_OP_CONCAT", op::translate_concat },
{"GGML_OP_CONT", op::translate_cont },
{"GGML_OP_DUP", op::translate_cont },
{"GGML_OP_DIV", op::translate_div },
{"GGML_OP_FILL", op::translate_fill },
{"GGML_OP_GET_ROWS", op::translate_get_rows },
{"GGML_OP_IM2COL", op::translate_im2col },
{"GGML_OP_IM2COL_3D", op::translate_im2col_3d },
{"GGML_OP_MUL", op::translate_1to1_match_2_inputs<v1::Multiply>},
{"GGML_OP_MUL_MAT", op::translate_mulmat },
{"GGML_OP_MUL_MAT_ID", op::translate_mul_mat_id },
{"GGML_OP_PERMUTE", op::translate_permute },
{"GGML_OP_RESHAPE", op::translate_reshape },
{"GGML_OP_RMS_NORM", op::translate_rms_norm },
{"GGML_OP_NORM", op::translate_norm },
{"GGML_OP_L2_NORM", op::translate_l2_norm },
{"GGML_OP_SUM_ROWS", op::translate_sum_rows },
{"GGML_OP_ROPE", op::translate_rope },
{"GGML_OP_SCALE", op::translate_scale },
{"GGML_OP_SQR", op::translate_sqr },
{"GGML_OP_SQRT", op::translate_sqrt },
{"GGML_OP_SOFT_MAX", op::translate_soft_max },
{"GGML_OP_ARGSORT", op::translate_argsort },
{"GGML_OP_SUB", op::translate_1to1_match_2_inputs<v1::Subtract>},
{"GGML_OP_TRANSPOSE", op::translate_transpose },
{"GGML_OP_SIN", op::translate_1to1_match_1_input<v0::Sin> },
{"GGML_OP_COS", op::translate_1to1_match_1_input<v0::Cos> },
{"GGML_OP_LOG", op::translate_1to1_match_1_input<v0::Log> },
{"GGML_OP_MEAN", op::translate_mean },
{"GGML_OP_SUM", op::translate_sum },
{"GGML_UNARY_OP_GELU", op::translate_unary_gelu },
{"GGML_UNARY_OP_GELU_ERF", op::translate_unary_gelu_erf },
{"GGML_UNARY_OP_GELU_QUICK", op::translate_unary_gelu_quick },
{"GGML_UNARY_OP_ELU", op::translate_unary_elu },
{"GGML_UNARY_OP_HARDSWISH", op::translate_1to1_match_1_input<v4::HSwish> },
{"GGML_UNARY_OP_HARDSIGMOID", op::translate_unary_hardsigmoid },
{"GGML_UNARY_OP_STEP", op::translate_unary_step },
{"GGML_UNARY_OP_ABS", op::translate_1to1_match_1_input<v0::Abs> },
{"GGML_UNARY_OP_SGN", op::translate_1to1_match_1_input<v0::Sign> },
{"GGML_UNARY_OP_FLOOR", op::translate_1to1_match_1_input<v0::Floor> },
{"GGML_UNARY_OP_CEIL", op::translate_1to1_match_1_input<v0::Ceiling> },
{"GGML_UNARY_OP_ROUND", op::translate_unary_round },
{"GGML_UNARY_OP_EXPM1", op::translate_unary_expm1 },
{"GGML_UNARY_OP_SIGMOID", op::translate_1to1_match_1_input<v0::Sigmoid> },
{"GGML_UNARY_OP_SILU", op::translate_1to1_match_1_input<v4::Swish> },
{"GGML_UNARY_OP_SOFTPLUS", op::translate_unary_softplus },
{"GGML_UNARY_OP_TANH", op::translate_1to1_match_1_input<v0::Tanh> },
{"GGML_UNARY_OP_EXP", op::translate_1to1_match_1_input<v0::Exp> },
{"GGML_UNARY_OP_NEG", op::translate_1to1_match_1_input<v0::Negative> },
{"GGML_UNARY_OP_RELU", op::translate_1to1_match_1_input<v0::Relu> },
{"GGML_OP_VIEW", op::translate_view },
{"GGML_GLU_OP_SWIGLU", op::translate_glu_swiglu },
{"GGML_GLU_OP_SWIGLU_OAI", op::translate_glu_swiglu_oai },
{"GGML_GLU_OP_SWIGLU_CLAMP", op::translate_glu_swiglu_clamp },
{"GGML_GLU_OP_GEGLU", op::translate_glu_geglu },
{"GGML_GLU_OP_GEGLU_QUICK", op::translate_glu_geglu_quick },
{"GGML_OP_SET_ROWS", op::translate_set_rows },
{"GGML_OP_CPY", op::translate_cpy },
{"GGML_OP_FLASH_ATTN_EXT", op::translate_flash_attn_ext },
{"GGML_OP_CLAMP", op::translate_clamp },
{"GGML_OP_PAD", op::translate_pad },
{"GGML_OP_SSM_CONV", op::translate_ssm_conv },
{"GGML_OP_GATED_DELTA_NET", op::translate_gated_delta_net },
{"GGML_OP_REPEAT", op::translate_repeat },
{"GGML_OP_CUMSUM", op::translate_cumsum },
{"GGML_OP_FILL", op::translate_fill },
{"GGML_OP_DIAG", op::translate_diag },
{"GGML_OP_TRI", op::translate_tri },
{"GGML_OP_SET", op::translate_set },
{"GGML_OP_POOL_2D", op::translate_pool_2d },
{"GGML_OP_ROLL", op::translate_roll },
{"GGML_OP_UPSCALE", op::translate_upscale },
{"GGML_OP_CONV_2D", op::translate_conv_2d },
{"GGML_OP_CONV_2D_DW", op::translate_conv_2d_dw },
{"GGML_OP_CONV_TRANSPOSE_1D", op::translate_conv_transpose_1d },
{"GGML_OP_CONV_TRANSPOSE_2D", op::translate_conv_transpose_2d },
{"GGML_OP_CONV_3D", op::translate_conv_3d },
// solve_tri has accuracy issues on GPU
// {"GGML_OP_SOLVE_TRI", op::translate_solve_tri },
};
}
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,83 @@
#pragma once
#include "node_context.h"
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
#define GGML_OP_CONVERTER(op) OutputVector op(const NodeContext & context)
GGML_OP_CONVERTER(translate_add);
GGML_OP_CONVERTER(translate_cont);
GGML_OP_CONVERTER(translate_concat);
GGML_OP_CONVERTER(translate_add_id);
GGML_OP_CONVERTER(translate_div);
GGML_OP_CONVERTER(translate_fill);
GGML_OP_CONVERTER(translate_get_rows);
GGML_OP_CONVERTER(translate_im2col);
GGML_OP_CONVERTER(translate_im2col_3d);
GGML_OP_CONVERTER(translate_mulmat);
GGML_OP_CONVERTER(translate_mul_mat_id);
GGML_OP_CONVERTER(translate_permute);
GGML_OP_CONVERTER(translate_reshape);
GGML_OP_CONVERTER(translate_rms_norm);
GGML_OP_CONVERTER(translate_norm);
GGML_OP_CONVERTER(translate_l2_norm);
GGML_OP_CONVERTER(translate_sum_rows);
GGML_OP_CONVERTER(translate_sqr);
GGML_OP_CONVERTER(translate_rope);
GGML_OP_CONVERTER(translate_scale);
GGML_OP_CONVERTER(translate_sqrt);
GGML_OP_CONVERTER(translate_unary_softplus);
GGML_OP_CONVERTER(translate_soft_max);
GGML_OP_CONVERTER(translate_transpose);
GGML_OP_CONVERTER(translate_view);
GGML_OP_CONVERTER(translate_glu_swiglu);
GGML_OP_CONVERTER(translate_glu_swiglu_oai);
GGML_OP_CONVERTER(translate_glu_swiglu_clamp);
GGML_OP_CONVERTER(translate_glu_geglu);
GGML_OP_CONVERTER(translate_glu_geglu_quick);
GGML_OP_CONVERTER(translate_set_rows);
GGML_OP_CONVERTER(translate_cpy);
GGML_OP_CONVERTER(translate_argsort);
GGML_OP_CONVERTER(translate_flash_attn_ext);
GGML_OP_CONVERTER(translate_clamp);
GGML_OP_CONVERTER(translate_pad);
GGML_OP_CONVERTER(translate_ssm_conv);
GGML_OP_CONVERTER(translate_gated_delta_net);
GGML_OP_CONVERTER(translate_repeat);
GGML_OP_CONVERTER(translate_cumsum);
GGML_OP_CONVERTER(translate_fill);
GGML_OP_CONVERTER(translate_set);
GGML_OP_CONVERTER(translate_diag);
GGML_OP_CONVERTER(translate_tri);
GGML_OP_CONVERTER(translate_solve_tri);
GGML_OP_CONVERTER(translate_pool_2d);
GGML_OP_CONVERTER(translate_roll);
GGML_OP_CONVERTER(translate_upscale);
GGML_OP_CONVERTER(translate_mean);
GGML_OP_CONVERTER(translate_sum);
GGML_OP_CONVERTER(translate_unary_gelu);
GGML_OP_CONVERTER(translate_unary_gelu_erf);
GGML_OP_CONVERTER(translate_unary_gelu_quick);
GGML_OP_CONVERTER(translate_unary_elu);
GGML_OP_CONVERTER(translate_unary_hardsigmoid);
GGML_OP_CONVERTER(translate_unary_step);
GGML_OP_CONVERTER(translate_unary_round);
GGML_OP_CONVERTER(translate_unary_expm1);
GGML_OP_CONVERTER(translate_conv_2d);
GGML_OP_CONVERTER(translate_conv_2d_dw);
GGML_OP_CONVERTER(translate_conv_transpose_1d);
GGML_OP_CONVERTER(translate_conv_transpose_2d);
GGML_OP_CONVERTER(translate_conv_3d);
} // namespace op
std::unordered_map<std::string, CreatorFunction> get_supported_ops();
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,96 @@
#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/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 {
// 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.
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)) {
return false;
}
for (const auto & input : node->input_values()) {
const auto * src = input.get_node();
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 (depth > 0 && is_rms_norm_output(input, depth - 1)) {
return true;
}
}
return false;
}
} // namespace
AlignEltwiseOperandRanks::AlignEltwiseOperandRanks() {
auto eltwise_m = ov::pass::pattern::wrap_type<ov::op::v1::Add, ov::op::v1::Multiply, ov::op::v1::Subtract>();
const auto callback = [this](ov::pass::pattern::Matcher & m) {
auto node = m.get_match_root();
if (node->get_input_size() != 2) {
return false;
}
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())) {
return false;
}
const auto lhs_rank = lhs.get_partial_shape().rank();
const auto rhs_rank = rhs.get_partial_shape().rank();
if (lhs_rank.is_dynamic() || rhs_rank.is_dynamic() || lhs_rank == rhs_rank) {
return false;
}
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.
if (is_rms_norm_output(shorter)) {
return false;
}
const int64_t diff = std::abs(lhs_rank.get_length() - rhs_rank.get_length());
std::vector<int64_t> axes(static_cast<size_t>(diff));
std::iota(axes.begin(), axes.end(), 0);
auto unsqueeze = std::make_shared<ov::op::v0::Unsqueeze>(
shorter, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{ axes.size() }, axes));
node->input(shorter_idx).replace_source_output(unsqueeze->output(0));
register_new_node(unsqueeze);
return true;
};
register_matcher(
std::make_shared<ov::pass::pattern::Matcher>(eltwise_m, "ov::frontend::ggml::pass::AlignEltwiseOperandRanks"),
callback);
}
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,39 @@
#pragma once
#include <openvino/pass/matcher_pass.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
// Give 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. gemma-4 dense hits
// this under stateful execution, where the layer tail adds a rank-3 residual to the rank-4
// RMS-norm output; the model then degenerates into repeated tokens on GPU while CPU is correct.
// Equalising the ranks keeps the fusion and makes it compute the right answer.
//
// Deliberately a graph pass rather than something the op translators do: rank is load-bearing
// during translation (several translators and later passes read operand ranks), and rewriting
// operands mid-translate breaks the attention path. Running after the graph is complete avoids
// that entirely.
//
// 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 rather than
// becoming a fused post-op, and is not affected by the defect. The norm output itself is never
// unsqueezed: when it is the lower-rank operand (gemma-3), that breaks the fused path instead.
class AlignEltwiseOperandRanks : public ov::pass::MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("ov::frontend::ggml::pass::AlignEltwiseOperandRanks")
AlignEltwiseOperandRanks();
};
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,70 @@
#include "fuse_argsort_topk.h"
#include <openvino/op/constant.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/topk.hpp>
#include <openvino/pass/pattern/op/wrap_type.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
FuseArgsortTopK::FuseArgsortTopK() {
// GGML argsort materializes the full ordering before a VIEW keeps the prefix.
// Let TopK produce only that prefix when all index consumers are such views.
auto pattern = ov::pass::pattern::wrap_type<ov::op::v11::TopK>();
const auto callback = [](ov::pass::pattern::Matcher & m) {
auto topk = ov::as_type_ptr<ov::op::v11::TopK>(m.get_match_root());
if (!topk->output(0).get_target_inputs().empty() || topk->output(1).get_target_inputs().empty() ||
topk->get_sort_type() != ov::op::v11::TopK::SortType::SORT_VALUES) {
return false;
}
const auto shape = topk->get_output_partial_shape(1);
if (shape.rank().is_dynamic()) {
return false;
}
const int64_t rank = shape.rank().get_length();
const int64_t axis = topk->get_axis();
if (shape[axis].is_dynamic()) {
return false;
}
int64_t prefix = -1;
for (const auto & input : topk->output(1).get_target_inputs()) {
const auto * slice = ov::as_type<ov::op::v8::Slice>(input.get_node());
if (!slice || input.get_index() != 0 || slice->get_input_size() != 5) {
return false;
}
int64_t values[4];
for (size_t i = 0; i < 4; ++i) {
auto value = ov::as_type_ptr<ov::op::v0::Constant>(slice->get_input_node_shared_ptr(i + 1));
if (!value || ov::shape_size(value->get_shape()) != 1) {
return false;
}
values[i] = value->cast_vector<int64_t>()[0];
}
const int64_t slice_axis = values[3] < 0 ? values[3] + rank : values[3];
if (values[0] != 0 || values[2] != 1 || slice_axis != axis || values[1] <= 0 ||
values[1] >= shape[axis].get_length() || (prefix != -1 && prefix != values[1])) {
return false;
}
prefix = values[1];
}
// ggml_argsort_top_k sorts the full row, then exposes only its first k indices.
// Keep other TopK forms unchanged because their ordering can be observable.
topk->set_argument(1, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {prefix}));
topk->validate_and_infer_types();
return true;
};
register_matcher(std::make_shared<ov::pass::pattern::Matcher>(pattern, "ov::frontend::ggml::pass::FuseArgsortTopK"), callback);
}
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,19 @@
#pragma once
#include <openvino/pass/matcher_pass.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
class FuseArgsortTopK : public ov::pass::MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("ov::frontend::ggml::pass::FuseArgsortTopK")
FuseArgsortTopK();
};
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,553 @@
#include "fuse_moe_compressed.h"
#include <cstring>
#include <limits>
#include <set>
#include <memory>
#include <openvino/core/graph_util.hpp>
#include <openvino/core/rt_info.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/gelu.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/reduce_sum.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/subtract.hpp>
#include <openvino/op/swish.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <openvino/pass/constant_folding.hpp>
#include <openvino/pass/pattern/op/optional.hpp>
#include <openvino/pass/pattern/op/wrap_type.hpp>
#include "../op/gather_matmul.hpp"
#include "../op/moe_compressed.hpp"
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
namespace {
struct dequant_inputs {
ov::Output<ov::Node> weight;
ov::Output<ov::Node> scale;
ov::Output<ov::Node> zp;
bool has_zp = false;
bool ok = false;
};
// Peel the chain built by make_int4_weights/make_int8_weights back to its Constant inputs.
// Grouped weights keep the pre-Reshape rank-4 form [n_expert, n, k/group, group] with scale
// and zp at [n_expert, n, k/group, 1], which is the layout MOECompressed expects. Channel-wise
// weights stay rank-3 with a rank-3 scale and carry no zp.
dequant_inputs unwrap_dequant(const ov::Output<ov::Node> & b) {
dequant_inputs res;
auto node = b.get_node_shared_ptr();
while (ov::is_type<ov::op::v0::Convert>(node) || ov::is_type<ov::op::v1::Reshape>(node)) {
node = node->get_input_node_shared_ptr(0);
}
auto mul = ov::as_type_ptr<ov::op::v1::Multiply>(node);
if (!mul) {
return res;
}
res.scale = mul->input_value(1);
auto lhs = mul->get_input_node_shared_ptr(0);
if (auto sub = ov::as_type_ptr<ov::op::v1::Subtract>(lhs)) {
// Take the zero point down to its Constant: an integer zp is wrapped in a Convert to f16,
// and the op wants the integer form. A natively quantized expert instead carries an exact
// f16 zp (-min/scale) with no integer behind it, which the MoE kernel does not accept.
auto zp_node = sub->get_input_node_shared_ptr(1);
while (ov::is_type<ov::op::v0::Convert>(zp_node)) {
zp_node = zp_node->get_input_node_shared_ptr(0);
}
res.zp = zp_node->output(0);
res.has_zp = true;
lhs = sub->get_input_node_shared_ptr(0);
}
while (ov::is_type<ov::op::v0::Convert>(lhs)) {
lhs = lhs->get_input_node_shared_ptr(0);
}
if (!ov::is_type<ov::op::v0::Constant>(lhs)) {
return res;
}
res.weight = lhs->output(0);
res.ok = res.scale.get_partial_shape().is_static() && res.weight.get_partial_shape().is_static();
return res;
}
size_t logical_k(const ov::Shape & shape) {
return shape.size() == 4 ? shape[2] * shape[3] : shape.back();
}
// Slice a [n_expert, m, ...] compressed weight/scale/zp Constant along axis 1 (m), [begin, end).
// Built by copying raw bytes directly instead of a graph Slice op: CommonOptimizations rewrites
// v8::Slice into v1::StridedSlice for constant folding regardless of disable_constant_folding
// (the rewrite doesn't carry the marking over), and that reference evaluator crashes on
// sub-byte (u4/i4) element types. Each axis-1 "row" (the trailing dims) is confirmed
// byte-aligned here -- weight/scale/zp trailing sizes are always whole groups -- so a
// byte-range memcpy per row is exact for both regular and sub-byte element types.
ov::Output<ov::Node> slice_experts_dim1(const ov::Output<ov::Node> & tensor, int64_t begin, int64_t end) {
auto constant = ov::as_type_ptr<ov::op::v0::Constant>(tensor.get_node_shared_ptr());
OPENVINO_ASSERT(constant, "slice_experts_dim1 expects a Constant input");
const auto & shape = constant->get_shape();
OPENVINO_ASSERT(shape.size() >= 2, "slice_experts_dim1 expects rank >= 2");
const auto & type = constant->get_element_type();
size_t row_elems = 1;
for (size_t i = 2; i < shape.size(); ++i) {
row_elems *= shape[i];
}
const size_t row_bits = row_elems * type.bitwidth();
OPENVINO_ASSERT(row_bits % 8 == 0, "slice_experts_dim1: row is not byte-aligned");
const size_t row_bytes = row_bits / 8;
ov::Shape out_shape = shape;
out_shape[1] = static_cast<size_t>(end - begin);
std::vector<uint8_t> out_data(shape[0] * static_cast<size_t>(end - begin) * row_bytes);
const auto * src = static_cast<const uint8_t *>(constant->get_data_ptr());
const size_t src_row_bytes = shape[1] * row_bytes;
for (size_t e = 0; e < shape[0]; ++e) {
std::memcpy(out_data.data() + e * static_cast<size_t>(end - begin) * row_bytes,
src + e * src_row_bytes + static_cast<size_t>(begin) * row_bytes,
static_cast<size_t>(end - begin) * row_bytes);
}
return std::make_shared<ov::op::v0::Constant>(type, out_shape, out_data.data());
}
} // namespace
FuseMoeCompressed::FuseMoeCompressed() {
using namespace ov::pass::pattern;
// The gate and up projections each get their own Reshape/Transpose of the hidden state and
// their own Reshape of the routing ids, so every branch needs its own sub-pattern. On GPU
// mul_mat_id also converts the activations to f16 before the op and back to f32 after it,
// so those Converts are matched as optional.
auto hidden_gate_m = any_input();
auto a_gate_reshape_m = wrap_type<ov::op::v1::Reshape>({ hidden_gate_m, any_input() });
auto a_gate_m =
wrap_type<ov::op::v1::Transpose>({ optional<ov::op::v0::Convert>({ a_gate_reshape_m }), any_input() });
auto hidden_up_m = any_input();
auto a_up_m = wrap_type<ov::op::v1::Transpose>(
{ optional<ov::op::v0::Convert>({ wrap_type<ov::op::v1::Reshape>({ hidden_up_m, any_input() }) }),
any_input() });
auto gate_w_m = any_input();
auto up_w_m = any_input();
auto down_w_m = any_input();
auto ids_gate_m = any_input();
auto ids_up_m = any_input();
auto ids_down_m = any_input();
auto bgm_gate_m = wrap_type<ov::op::internal::GatherMatmul>({ a_gate_m, gate_w_m, ids_gate_m, any_input() });
auto gate_u_m = optional<ov::op::v0::Convert>({ wrap_type<ov::op::v0::Unsqueeze>(
{ wrap_type<ov::op::v1::Transpose>({ bgm_gate_m, any_input() }), any_input() }) });
auto silu_m = wrap_type<ov::op::v4::Swish>({ gate_u_m });
auto bgm_up_m = wrap_type<ov::op::internal::GatherMatmul>({ a_up_m, up_w_m, ids_up_m, any_input() });
auto up_u_m = optional<ov::op::v0::Convert>({ wrap_type<ov::op::v0::Unsqueeze>(
{ wrap_type<ov::op::v1::Transpose>({ bgm_up_m, any_input() }), any_input() }) });
auto swiglu_m = wrap_type<ov::op::v1::Multiply>({ silu_m, up_u_m });
auto d_t_m = wrap_type<ov::op::v1::Transpose>(
{ optional<ov::op::v0::Convert>({ wrap_type<ov::op::v1::Reshape>({ swiglu_m, any_input() }) }),
any_input() });
auto bgm_down_m = wrap_type<ov::op::internal::GatherMatmul>({ d_t_m, down_w_m, ids_down_m, any_input() });
auto down_u_m = optional<ov::op::v0::Convert>({ wrap_type<ov::op::v0::Unsqueeze>(
{ wrap_type<ov::op::v1::Transpose>({ bgm_down_m, any_input() }), any_input() }) });
auto routing_m = any_input();
auto weighted_m = wrap_type<ov::op::v1::Multiply>({ down_u_m, routing_m });
auto root_m = wrap_type<ov::op::v1::ReduceSum>({ weighted_m, any_input() });
const auto callback = [=](Matcher & m) {
auto & pm = m.get_pattern_value_map();
const auto gate = unwrap_dequant(pm.at(gate_w_m));
const auto up = unwrap_dequant(pm.at(up_w_m));
const auto down = unwrap_dequant(pm.at(down_w_m));
if (!gate.ok || !up.ok || !down.ok) {
return false;
}
const auto gate_shape = gate.weight.get_shape();
const auto up_shape = up.weight.get_shape();
const auto down_shape = down.weight.get_shape();
if (gate_shape != up_shape || gate_shape.size() < 3 || down_shape.size() < 3) {
return false;
}
// MOECompressed carries one group_size and one has_zp for all three projections, so a
// model whose down-proj is quantized differently from gate/up cannot be described. This
// happens when ggml requantizes Q5_K/Q6_K experts to channel-wise int8.
if (gate.has_zp != down.has_zp || gate_shape.size() != down_shape.size()) {
return false;
}
// The kernel only takes an integer zero point (moe_3gemm_swiglu_opt validate_impl).
if (gate.has_zp) {
static const std::set<ov::element::Type> int_zp_types = { ov::element::u4, ov::element::i4,
ov::element::u8, ov::element::i8 };
if (int_zp_types.count(gate.zp.get_element_type()) == 0 ||
int_zp_types.count(down.zp.get_element_type()) == 0) {
return false;
}
}
// Config holds a single group_size for all three projections.
const auto group_of = [](const dequant_inputs & w) {
const auto s = w.weight.get_shape();
return s.size() == 4 ? s[3] : logical_k(s);
};
if (group_of(gate) != group_of(up) || group_of(gate) != group_of(down)) {
return false;
}
// all three branches must route the same hidden state through the same experts
if (pm.at(hidden_gate_m) != pm.at(hidden_up_m)) {
return false;
}
auto ids = pm.at(ids_down_m);
const auto ids_pshape = ids.get_partial_shape();
if (ids_pshape.rank().is_dynamic() || ids_pshape[ids_pshape.rank().get_length() - 1].is_dynamic()) {
return false;
}
const size_t top_k = ids_pshape[ids_pshape.rank().get_length() - 1].get_length();
// routing weights arrive as [1, n_tokens, top_k, 1]; the op wants [..., top_k]
auto routing = pm.at(routing_m);
const auto routing_pshape = routing.get_partial_shape();
if (routing_pshape.rank().is_dynamic() || routing_pshape.rank().get_length() != 4 ||
routing_pshape[3] != 1) {
return false;
}
// MOE requires routing weights and ids to have the same shape. Drop the trailing 1 of the
// routing weights and give the ids the leading batch dim, so both become [1, n_tokens, top_k].
routing = std::make_shared<ov::op::v0::Squeeze>(
routing, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{ 1 }, { 3 }));
if (ids_pshape.rank().get_length() == 2) {
ids = std::make_shared<ov::op::v0::Unsqueeze>(
ids, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{ 1 }, { 0 }));
}
if (routing.get_partial_shape() != ids.get_partial_shape()) {
return false;
}
const size_t down_k = logical_k(down_shape);
const auto down_scale_shape = down.scale.get_shape();
const size_t down_groups = down_scale_shape.size() >= 3 ? down_scale_shape[2] : 1;
ov::op::internal::MOECompressed::Config config;
config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU;
config.activation_type = ov::op::internal::MOE::Activation_type::SWIGLU;
config.expert_alpha = 0.0f;
config.expert_beta = 1.0f;
config.gate_idx = 0;
config.hidden_size = logical_k(gate_shape);
config.inter_size = gate_shape[1];
config.num_expert = gate_shape[0];
config.num_shared_expert = 0;
config.top_k = top_k;
config.group_size = down_groups <= 1 ? std::numeric_limits<size_t>::max() : down_k / down_groups;
config.has_batch_dim = true;
config.has_zp = gate.has_zp;
// dynamic makes the output follow the hidden state, so the plugin can lower this region
// to f16 together with the rest of the graph
config.out_type = ov::element::dynamic;
auto absent_zp = [] {
auto zp = std::make_shared<ov::op::v0::Constant>(ov::element::dynamic, ov::Shape{ 0 });
ov::pass::disable_constant_folding(zp);
return zp->output(0);
};
// MOE takes its output type from the hidden state. Transpose the activations before the
// f16 Convert that mul_mat_id adds on GPU, so the op stays f32 like the block it replaces
// and the plugin can lower the whole region uniformly.
const auto a_transpose = pm.at(a_gate_m).get_node_shared_ptr();
ov::Output<ov::Node> hidden =
std::make_shared<ov::op::v1::Transpose>(pm.at(a_gate_reshape_m), a_transpose->input_value(1));
const ov::OutputVector args = {
hidden, routing, ids,
gate.weight, gate.scale, gate.has_zp ? gate.zp : absent_zp(),
up.weight, up.scale, up.has_zp ? up.zp : absent_zp(),
down.weight, down.scale, down.has_zp ? down.zp : absent_zp(),
};
auto moe = std::make_shared<ov::op::internal::MOECompressed>(args, config);
// MOE takes its output type from the hidden state, which is f16 on GPU, while the rest of
// the ggml graph works in f32.
ov::Output<ov::Node> result = moe->output(0);
const auto root_type = m.get_match_root()->get_output_element_type(0);
if (result.get_element_type() != root_type) {
result = std::make_shared<ov::op::v0::Convert>(result, root_type);
}
result.get_node_shared_ptr()->set_friendly_name(m.get_match_root()->get_friendly_name());
ov::copy_runtime_info(m.get_matched_nodes(), result.get_node_shared_ptr());
ov::replace_node(m.get_match_root(), result.get_node_shared_ptr());
register_new_node(moe);
return true;
};
register_matcher(std::make_shared<Matcher>(root_m, "ov::frontend::ggml::pass::FuseMoeCompressed"), callback);
}
FuseMoeCompressedFusedGateUp::FuseMoeCompressedFusedGateUp() {
using namespace ov::pass::pattern;
// A single GatherMatmul computes the fused gate+up projection; the gate/up split happens
// AFTER the GEMM via two Slice ops on the last (feature) axis (see get_glu_inputs in
// op/glu_geglu.cpp), unlike FuseMoeCompressed's two-separate-GatherMatmul models.
auto hidden_m = any_input();
auto a_reshape_m = wrap_type<ov::op::v1::Reshape>({ hidden_m, any_input() });
auto a_m = wrap_type<ov::op::v1::Transpose>({ optional<ov::op::v0::Convert>({ a_reshape_m }), any_input() });
auto gate_up_w_m = any_input();
auto ids_gate_up_m = any_input();
auto bgm_fused_m = wrap_type<ov::op::internal::GatherMatmul>({ a_m, gate_up_w_m, ids_gate_up_m, any_input() });
// Two graph shapes reach here. The rank-4 (stateless) graph restores the batch dim with an
// Unsqueeze after the Transpose and may Convert afterwards:
// GatherMatmul -> Transpose -> Unsqueeze -> [Convert] -> Slice
// The rank-3 (stateful) graph never drops to a batch dim at all, so there is no Unsqueeze
// here - it appears later, between the GEGLU and the down Reshape - and the Convert sits on
// the other side of the Transpose:
// GatherMatmul -> Convert -> Transpose -> Slice
// optional<T>({a, b}) matches T(a, b) or bare a, so one pattern covers both.
auto gu_t_m =
wrap_type<ov::op::v1::Transpose>({ optional<ov::op::v0::Convert>({ bgm_fused_m }), any_input() });
auto gu_u_m =
optional<ov::op::v0::Convert>({ optional<ov::op::v0::Unsqueeze>({ gu_t_m, any_input() }) });
auto gate_slice_m = wrap_type<ov::op::v8::Slice>({ gu_u_m, any_input(), any_input(), any_input(), any_input() });
auto up_slice_m = wrap_type<ov::op::v8::Slice>({ gu_u_m, any_input(), any_input(), any_input(), any_input() });
auto gelu_m = wrap_type<ov::op::v7::Gelu>({ gate_slice_m });
auto geglu_m = wrap_type<ov::op::v1::Multiply>({ gelu_m, up_slice_m });
// The rank-3 graph inserts the Unsqueeze the gate/up branch lacked right here, before the
// Reshape that feeds the down projection; the rank-4 graph goes straight from the GEGLU
// into the Reshape.
auto d_t_m = wrap_type<ov::op::v1::Transpose>(
{ optional<ov::op::v0::Convert>({ wrap_type<ov::op::v1::Reshape>(
{ optional<ov::op::v0::Unsqueeze>({ geglu_m, any_input() }), any_input() }) }),
any_input() });
auto down_w_m = any_input();
auto ids_down_m = any_input();
auto bgm_down_m = wrap_type<ov::op::internal::GatherMatmul>({ d_t_m, down_w_m, ids_down_m, any_input() });
// Same two shapes as the gate/up branch above.
auto down_t_m =
wrap_type<ov::op::v1::Transpose>({ optional<ov::op::v0::Convert>({ bgm_down_m }), any_input() });
auto down_u_m =
optional<ov::op::v0::Convert>({ optional<ov::op::v0::Unsqueeze>({ down_t_m, any_input() }) });
// gemma-4 applies an extra per-expert output scale to the down projection before the
// router-weight multiply (llama-graph.cpp's ffn_down_exps.scale); FuseMoeCompressed's
// Qwen-shaped models have no such scale, so this node is specific to this pattern.
auto down_scale_m = any_input();
auto down_scaled_m = wrap_type<ov::op::v1::Multiply>({ down_u_m, down_scale_m });
auto routing_m = any_input();
auto weighted_m = wrap_type<ov::op::v1::Multiply>({ down_scaled_m, routing_m });
auto root_m = wrap_type<ov::op::v1::ReduceSum>({ weighted_m, any_input() });
const auto callback = [=](Matcher & m) {
auto & pm = m.get_pattern_value_map();
const auto gate_up = unwrap_dequant(pm.at(gate_up_w_m));
const auto down = unwrap_dequant(pm.at(down_w_m));
if (!gate_up.ok || !down.ok) {
return false;
}
// oneDNN's weight-decompression GEMM needs a real zero point, so a symmetric (no zp)
// projection cannot be fused here -- e.g. the GPU-only Q4_0 fallback in
// ggml_openvino_get_extracted_layout for grouped 8-bit expert weights.
if (!gate_up.has_zp || !down.has_zp) {
return false;
}
const auto gate_up_shape = gate_up.weight.get_shape();
const auto down_shape = down.weight.get_shape();
if (gate_up_shape.size() < 3 || down_shape.size() < 3 || gate_up_shape.size() != down_shape.size()) {
return false;
}
// The op reads the zero point straight off a weight port, so it must already be an
// integer Constant. Requantized experts (GGML_OPENVINO_REQUANT_KQUANT=q4_asym64_all)
// are; a native Q4_K expert keeps an exact f16 zp and is left to the unfused path
// rather than rounded here -- rounding would put a live node on that port and the
// expert GEMM would read the wrong zero point.
static const std::set<ov::element::Type> int_types = { ov::element::u4, ov::element::i4,
ov::element::u8, ov::element::i8 };
if (int_types.count(gate_up.weight.get_element_type()) == 0 ||
int_types.count(down.weight.get_element_type()) == 0 ||
int_types.count(gate_up.zp.get_element_type()) == 0 ||
int_types.count(down.zp.get_element_type()) == 0) {
return false;
}
const auto group_of = [](const dequant_inputs & w) {
const auto s = w.weight.get_shape();
return s.size() == 4 ? s[3] : logical_k(s);
};
if (group_of(gate_up) != group_of(down)) {
return false;
}
auto gelu_node = ov::as_type_ptr<ov::op::v7::Gelu>(pm.at(gelu_m).get_node_shared_ptr());
if (!gelu_node || gelu_node->get_approximation_mode() != ov::op::GeluApproximationMode::ERF) {
return false;
}
// Read the actual gate/up split point from the matched Slice nodes instead of assuming
// inter_size/2, so this stays correct if the model's ffn_dim convention ever changes.
auto gate_slice = ov::as_type_ptr<ov::op::v8::Slice>(pm.at(gate_slice_m).get_node_shared_ptr());
auto up_slice = ov::as_type_ptr<ov::op::v8::Slice>(pm.at(up_slice_m).get_node_shared_ptr());
auto gate_end_c = ov::as_type_ptr<ov::op::v0::Constant>(gate_slice->input_value(2).get_node_shared_ptr());
auto up_begin_c = ov::as_type_ptr<ov::op::v0::Constant>(up_slice->input_value(1).get_node_shared_ptr());
if (!gate_end_c || !up_begin_c) {
return false;
}
const auto gate_end = gate_end_c->cast_vector<int64_t>().at(0);
const auto up_begin = up_begin_c->cast_vector<int64_t>().at(0);
if (gate_end != up_begin || gate_end <= 0 || gate_end >= static_cast<int64_t>(gate_up_shape[1])) {
return false;
}
const int64_t mid = gate_end;
const int64_t full = static_cast<int64_t>(gate_up_shape[1]);
auto ids = pm.at(ids_down_m);
const auto ids_pshape = ids.get_partial_shape();
if (ids_pshape.rank().is_dynamic() || ids_pshape[ids_pshape.rank().get_length() - 1].is_dynamic()) {
return false;
}
const size_t top_k = ids_pshape[ids_pshape.rank().get_length() - 1].get_length();
// Routing weights arrive as [1, n_tokens, top_k, 1] on the rank-4 (stateless) graph and
// as [n_tokens, top_k, 1] on the rank-3 (stateful) one, which is the same thing without
// the leading batch dim. Normalise the rank-3 form up to the rank-4 one so everything
// below - and the op's own config - stays in the shape that is already validated on the
// stateless path; the batch dim is taken back off the result at the end.
const auto routing_in = pm.at(routing_m);
const auto routing_pshape = routing_in.get_partial_shape();
const auto routing_rank = routing_pshape.rank();
if (routing_rank.is_dynamic() || (routing_rank.get_length() != 4 && routing_rank.get_length() != 3) ||
routing_pshape[routing_rank.get_length() - 1] != 1) {
return false;
}
const bool batchless = routing_rank.get_length() == 3;
// Fold gemma-4's per-expert output scale into the routing weights: the reduction is
// sum_e(routing[e] * scale[e] * down_out[e]), and MOECompressed only takes one
// per-expert weight, so pre-multiply it into routing here (same [.., top_k, 1] shape).
auto down_scale = pm.at(down_scale_m);
if (down_scale.get_partial_shape() != routing_pshape) {
return false;
}
ov::Output<ov::Node> routing = std::make_shared<ov::op::v1::Multiply>(routing_in, down_scale);
if (batchless) {
routing = std::make_shared<ov::op::v0::Unsqueeze>(
routing, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{ 1 }, { 0 }));
}
routing = std::make_shared<ov::op::v0::Squeeze>(
routing, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{ 1 }, { 3 }));
if (ids_pshape.rank().get_length() == 2) {
ids = std::make_shared<ov::op::v0::Unsqueeze>(
ids, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{ 1 }, { 0 }));
}
if (routing.get_partial_shape() != ids.get_partial_shape()) {
return false;
}
const size_t down_k = logical_k(down_shape);
const auto down_scale_shape = down.scale.get_shape();
const size_t down_groups = down_scale_shape.size() >= 3 ? down_scale_shape[2] : 1;
// Split the fused gate_up weight/scale/zp on the output axis. slice_experts_dim1 copies
// raw bytes out of the Constant, so each half stays a Constant -- which is what the op
// needs on its weight ports.
const ov::Output<ov::Node> gate_weight = slice_experts_dim1(gate_up.weight, 0, mid);
const ov::Output<ov::Node> up_weight = slice_experts_dim1(gate_up.weight, mid, full);
const ov::Output<ov::Node> gate_scale = slice_experts_dim1(gate_up.scale, 0, mid);
const ov::Output<ov::Node> up_scale = slice_experts_dim1(gate_up.scale, mid, full);
const ov::Output<ov::Node> gate_zp = slice_experts_dim1(gate_up.zp, 0, mid);
const ov::Output<ov::Node> up_zp = slice_experts_dim1(gate_up.zp, mid, full);
ov::op::internal::MOECompressed::Config config;
config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU;
config.activation_type = ov::op::internal::MOE::Activation_type::GEGLU_ERF;
config.expert_alpha = 0.0f;
config.expert_beta = 1.0f;
config.gate_idx = 0;
config.hidden_size = logical_k(gate_up_shape);
config.inter_size = static_cast<size_t>(mid);
config.num_expert = gate_up_shape[0];
config.num_shared_expert = 0;
config.top_k = top_k;
config.group_size = down_groups <= 1 ? std::numeric_limits<size_t>::max() : down_k / down_groups;
config.has_batch_dim = true;
config.has_zp = true;
config.out_type = ov::element::dynamic;
// Rebuild the activations Transpose before mul_mat_id's f16 GPU Convert, so the op
// stays f32 like the block it replaces (mirrors FuseMoeCompressed).
const auto a_transpose = pm.at(a_m).get_node_shared_ptr();
ov::Output<ov::Node> hidden =
std::make_shared<ov::op::v1::Transpose>(pm.at(a_reshape_m), a_transpose->input_value(1));
// w0 is the activated (gate) lane and w1 the multiplied (up) lane -- see
// moe_3gemm_swiglu_opt.cpp: "scratch.up = up(x) * silu(gate(x))".
const ov::OutputVector args = {
hidden, routing, ids,
gate_weight, gate_scale, gate_zp,
up_weight, up_scale, up_zp,
down.weight, down.scale, down.zp,
};
auto moe = std::make_shared<ov::op::internal::MOECompressed>(args, config);
ov::Output<ov::Node> result = moe->output(0);
// The op was fed the batched form, so it produces [1, n_tokens, hidden]. On the rank-3
// graph the ReduceSum being replaced is [n_tokens, hidden], so drop the batch dim again.
if (batchless) {
result = std::make_shared<ov::op::v0::Squeeze>(
result, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{ 1 }, { 0 }));
}
const auto root_type = m.get_match_root()->get_output_element_type(0);
if (result.get_element_type() != root_type) {
result = std::make_shared<ov::op::v0::Convert>(result, root_type);
}
if (result.get_partial_shape() != m.get_match_root()->get_output_partial_shape(0)) {
return false;
}
result.get_node_shared_ptr()->set_friendly_name(m.get_match_root()->get_friendly_name());
ov::copy_runtime_info(m.get_matched_nodes(), result.get_node_shared_ptr());
ov::replace_node(m.get_match_root(), result.get_node_shared_ptr());
register_new_node(moe);
return true;
};
register_matcher(
std::make_shared<Matcher>(root_m, "ov::frontend::ggml::pass::FuseMoeCompressedFusedGateUp"), callback);
}
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,30 @@
#include "openvino/pass/matcher_pass.hpp"
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
// Folds the MoE expert block emitted for MUL_MAT_ID (3 GatherMatmul + SwiGLU + routing
// weighting + expert reduction) into a single ov::op::internal::MOECompressed.
class FuseMoeCompressed : public ov::pass::MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("ov::frontend::ggml::pass::FuseMoeCompressed")
FuseMoeCompressed();
};
// Folds the MoE expert block emitted for a model whose gate and up projections share one
// fused MUL_MAT_ID weight (gemma-4: one GatherMatmul + Slice/Slice split, GEGLU activation)
// into a single ov::op::internal::MOECompressed, GEMM3_SWIGLU/GEGLU_ERF. Splits the fused
// weight/scale/zp into gate/up halves so it lands on the same 3-GEMM expert-grouped kernel
// FuseMoeCompressed uses for models with separate gate/up weights.
class FuseMoeCompressedFusedGateUp : public ov::pass::MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("ov::frontend::ggml::pass::FuseMoeCompressedFusedGateUp")
FuseMoeCompressedFusedGateUp();
};
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,153 @@
#include "fuse_moe_router.h"
#include <openvino/core/graph_util.hpp>
#include <openvino/core/rt_info.hpp>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/clamp.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/reduce_sum.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/softmax.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/tile.hpp>
#include <openvino/op/topk.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <openvino/pass/pattern/op/wrap_type.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
namespace {
bool constant_is(const ov::Output<ov::Node> & output, const std::vector<int64_t> & values) {
auto node = ov::as_type_ptr<ov::op::v0::Constant>(output.get_node_shared_ptr());
return node && node->get_element_type().is_integral_number() && node->cast_vector<int64_t>() == values;
}
} // namespace
FuseMoeRouter::FuseMoeRouter() {
using namespace ov::pass::pattern;
using namespace ov::op;
// Match the GGML rank-4 routing chain. The GPU plugin recognizes the resulting
// Softmax -> TopK -> ReduceSum -> Divide subgraph as MoERouterFused.
auto logits = wrap_type<v0::MatMul>();
auto softmax = wrap_type<v8::Softmax>({logits});
auto topk = wrap_type<v11::TopK>({softmax, any_input()});
topk->set_output_size(2);
auto ids = wrap_type<v8::Slice>({topk->output(1), any_input(), any_input(), any_input(), any_input()});
auto ids_2d = wrap_type<v0::Squeeze>({ids, any_input()});
auto probs = wrap_type<v1::Reshape>({softmax, any_input()});
auto data = wrap_type<v0::Squeeze>({probs, any_input()});
auto data_batch = wrap_type<v8::Gather>({wrap_type<v3::ShapeOf>({data}), any_input(), any_input()});
auto ids_count = wrap_type<v8::Gather>({wrap_type<v3::ShapeOf>({ids_2d}), any_input(), any_input()});
auto target = wrap_type<v0::Concat>({data_batch, ids_count});
auto broadcast = wrap_type<v3::Broadcast>({ids_2d, target});
auto gather = wrap_type<v8::Gather>({data, broadcast, any_input()});
auto weights_4d = wrap_type<v0::Unsqueeze>({gather, any_input()});
auto weights = wrap_type<v1::Reshape>({weights_4d, any_input()});
auto sum = wrap_type<v1::ReduceSum>({weights, any_input()});
auto clamp = wrap_type<v0::Clamp>({sum});
auto tile = wrap_type<v0::Tile>({clamp, any_input()});
auto norm = wrap_type<v1::Divide>({weights, tile});
const auto callback = [=](Matcher & m) {
const auto & pm = m.get_pattern_value_map();
const auto node = [&](const std::shared_ptr<ov::Node> & p) { return pm.at(p).get_node_shared_ptr(); };
const auto input_is = [&](const std::shared_ptr<ov::Node> & p, size_t i, const std::vector<int64_t> & v) {
return constant_is(node(p)->input_value(i), v);
};
auto mm = ov::as_type_ptr<v0::MatMul>(node(logits));
auto sm = ov::as_type_ptr<v8::Softmax>(node(softmax));
auto tk = ov::as_type_ptr<v11::TopK>(node(topk));
auto reduce = ov::as_type_ptr<v1::ReduceSum>(node(sum));
auto limit = ov::as_type_ptr<v0::Clamp>(node(clamp));
const auto shape = mm->get_output_partial_shape(0);
if (shape.rank() != 4 || shape[0] != 1 || shape[1] != 1 || shape[3].is_dynamic() ||
mm->get_transpose_a() || mm->get_input_partial_shape(0).rank() != 4 ||
mm->get_input_partial_shape(1).rank() != 2 || (sm->get_axis() != -1 && sm->get_axis() != 3) ||
tk->get_axis() != 3 || tk->get_mode() != v11::TopK::Mode::MAX ||
tk->get_sort_type() != v11::TopK::SortType::SORT_VALUES || tk->get_stable() ||
tk->get_index_element_type() != ov::element::i32 ||
!tk->output(0).get_target_inputs().empty()) {
return false;
}
const int64_t experts = shape[3].get_length();
auto end = ov::as_type_ptr<v0::Constant>(node(ids)->get_input_node_shared_ptr(2));
if (!end || !end->get_element_type().is_integral_number() || ov::shape_size(end->get_shape()) != 1) {
return false;
}
const int64_t k = end->cast_vector<int64_t>()[0];
const auto sorted_shape = tk->get_output_partial_shape(1);
if (k <= 0 || k > experts || sorted_shape[3].is_dynamic() || sorted_shape[3].get_length() < k ||
!input_is(probs, 1, {1, -1, experts, 1}) ||
!input_is(data, 1, {0}) || !input_is(ids_2d, 1, {0, 1}) ||
!input_is(weights_4d, 1, {0}) || !input_is(weights, 1, {1, 1, -1, k}) ||
ov::as_type_ptr<v1::Reshape>(node(probs))->get_special_zero() ||
ov::as_type_ptr<v1::Reshape>(node(weights))->get_special_zero() ||
!input_is(gather, 2, {1}) || ov::as_type_ptr<v8::Gather>(node(gather))->get_batch_dims() != 1 ||
!input_is(data_batch, 1, {0}) || !input_is(data_batch, 2, {0}) ||
!input_is(ids_count, 1, {1}) || !input_is(ids_count, 2, {0}) ||
ov::as_type_ptr<v8::Gather>(node(data_batch))->get_batch_dims() != 0 ||
ov::as_type_ptr<v8::Gather>(node(ids_count))->get_batch_dims() != 0 ||
ov::as_type_ptr<v0::Concat>(node(target))->get_axis() != 0 ||
ov::as_type_ptr<v3::Broadcast>(node(broadcast))->get_broadcast_spec().m_type != ov::op::BroadcastType::BIDIRECTIONAL ||
!reduce->get_keep_dims() || (!input_is(sum, 1, {-1}) && !input_is(sum, 1, {3})) ||
!input_is(tile, 1, {1, 1, 1, k}) ||
ov::as_type_ptr<v1::Divide>(node(norm))->get_autob().m_type != ov::op::AutoBroadcastType::NUMPY) {
return false;
}
// The top-k softmax sum is at least k / experts. Leave margin for rounding.
// This guard is only valid for unbiased softmax routing.
if (!(limit->get_min() <= 0.5 * double(k) / experts && limit->get_max() >= 2.0)) {
return false;
}
std::vector<std::shared_ptr<ov::Node>> slices;
for (const auto & input : tk->output(1).get_target_inputs()) {
auto slice = ov::as_type_ptr<v8::Slice>(input.get_node()->shared_from_this());
if (!slice || input.get_index() != 0 || slice->get_input_size() != 5 ||
!constant_is(slice->input_value(1), {0}) || !constant_is(slice->input_value(2), {k}) ||
!constant_is(slice->input_value(3), {1}) ||
(!constant_is(slice->input_value(4), {3}) && !constant_is(slice->input_value(4), {-1}))) {
return false;
}
slices.push_back(slice);
}
// Remove GGML's leading singleton dimension while building the plugin pattern,
// then restore it on both outputs for the following GGML nodes.
auto axis0 = v0::Constant::create(ov::element::i64, ov::Shape{1}, {0});
auto hidden = std::make_shared<v0::Squeeze>(mm->input_value(0), axis0);
auto routing = std::make_shared<v0::MatMul>(hidden, mm->input_value(1), false, mm->get_transpose_b());
auto probabilities = std::make_shared<v8::Softmax>(routing, -1);
auto selected = std::make_shared<v11::TopK>(probabilities,
v0::Constant::create(ov::element::i64, ov::Shape{}, {k}), 2,
v11::TopK::Mode::MAX, v11::TopK::SortType::SORT_VALUES, tk->get_index_element_type(), false);
auto total = std::make_shared<v1::ReduceSum>(selected->output(0),
v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}), true);
auto normalized = std::make_shared<v1::Divide>(selected->output(0), total);
auto weights_out = std::make_shared<v0::Unsqueeze>(normalized, axis0);
auto ids_out = std::make_shared<v0::Unsqueeze>(selected->output(1), axis0);
ov::copy_runtime_info({mm, sm, tk, node(norm)}, {hidden, routing, probabilities, selected, total, normalized, weights_out, ids_out});
ov::replace_node(node(norm), weights_out);
for (const auto & slice : slices) {
ov::replace_node(slice, ids_out);
}
return true;
};
register_matcher(std::make_shared<Matcher>(norm, "ov::frontend::ggml::pass::FuseMoeRouter"), callback);
}
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,19 @@
#pragma once
#include <openvino/pass/matcher_pass.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
class FuseMoeRouter : public ov::pass::MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("ov::frontend::ggml::pass::FuseMoeRouter")
FuseMoeRouter();
};
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,283 @@
#include "fuse_to_conv.h"
#include <openvino/core/graph_util.hpp>
#include <openvino/core/rt_info.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/convolution.hpp>
#include <openvino/op/extractimagepatches.hpp>
#include <openvino/op/group_conv.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/pad.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/pass/pattern/op/pattern.hpp>
#include <openvino/pass/pattern/op/wrap_type.hpp>
#include <utility>
namespace opp = ov::pass::pattern;
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
FuseToConv::FuseToConv() {
const auto m_in0 = opp::any_input();
const auto m_in1 = opp::any_input();
const auto m_matmul = opp::wrap_type<ov::op::v0::MatMul>({m_in0, m_in1});
const auto callback = [=](opp::Matcher & m) {
auto matmul_node = ov::as_type_ptr<ov::op::v0::MatMul>(m.get_match_root());
if (!matmul_node) {
return false;
}
auto unwrap = [](ov::Output<Node> n) {
while (ov::is_type<ov::op::v0::Convert>(n.get_node_shared_ptr()) ||
ov::is_type<ov::op::v1::Reshape>(n.get_node_shared_ptr())) {
n = n.get_node_shared_ptr()->input_value(0);
}
return n;
};
auto get_im2col = [&](ov::Output<Node> trace)
-> std::pair<std::shared_ptr<ov::op::v3::ExtractImagePatches>, std::shared_ptr<ov::op::v1::Pad>> {
auto t2 = ov::as_type_ptr<ov::op::v1::Transpose>(unwrap(std::move(trace)).get_node_shared_ptr());
auto r1 = t2 ? ov::as_type_ptr<ov::op::v1::Reshape>(t2->get_input_node_shared_ptr(0)) : nullptr;
auto t1 = r1 ? ov::as_type_ptr<ov::op::v1::Transpose>(r1->get_input_node_shared_ptr(0)) : nullptr;
auto eip =
t1 ? ov::as_type_ptr<ov::op::v3::ExtractImagePatches>(t1->get_input_node_shared_ptr(0)) : nullptr;
auto pad = eip ? ov::as_type_ptr<ov::op::v1::Pad>(eip->get_input_node_shared_ptr(0)) : nullptr;
return {eip, pad};
};
bool weight_is_in0 = true;
auto [eip, pad] = get_im2col(matmul_node->input_value(1));
ov::Output<Node> w_trace = matmul_node->input_value(0);
if (!pad) {
std::tie(eip, pad) = get_im2col(matmul_node->input_value(0));
w_trace = matmul_node->input_value(1);
weight_is_in0 = false;
if (!pad) {
return false;
}
}
auto pb_const = ov::as_type_ptr<ov::op::v0::Constant>(pad->get_input_node_shared_ptr(1));
auto pe_const = ov::as_type_ptr<ov::op::v0::Constant>(pad->get_input_node_shared_ptr(2));
if (!pb_const || !pe_const) {
return false;
}
const auto pb = pb_const->cast_vector<std::ptrdiff_t>();
const auto pe = pe_const->cast_vector<std::ptrdiff_t>();
if (pb.size() < 4 || pe.size() < 4) {
return false;
}
auto image_input = pad->input_value(0);
auto image_shape = image_input.get_partial_shape();
if (image_shape.rank() != 4 || image_shape[1].is_dynamic()) {
return false;
}
const size_t IC = static_cast<size_t>(image_shape[1].get_length());
w_trace = unwrap(w_trace);
auto weight_pshape = w_trace.get_partial_shape();
if (!weight_pshape.is_static()) {
return false;
}
const auto & ws = weight_pshape.to_shape();
const size_t KH = eip->get_sizes()[0];
const size_t KW = eip->get_sizes()[1];
const size_t kernel_spatial_ic = IC * KH * KW;
size_t groups = 0;
if (IC == 1 && image_shape[0].is_static() && image_shape[2].is_static() && image_shape[3].is_static()) {
if ((ws.size() == 4 && ws[1] == 1 && ws[2] == KH && ws[3] == KW && ws[0] > 1) ||
(ws.size() == 2 && ws[1] == KH * KW && ws[0] > 1) ||
(ws.size() == 3 && ws[1] == 1 && ws[2] == KH * KW && ws[0] > 1)) {
groups = ws[0];
} else if (ws.size() == 4 && ws[0] == 1 && ws[2] == 1 && ws[3] == KH * KW && ws[1] > 1) {
groups = ws[1];
}
}
const bool is_depthwise = groups > 1 && (image_shape[0].get_length() % groups == 0);
ov::Output<Node> conv_out;
size_t OC = 0;
if (is_depthwise) {
const size_t N = static_cast<size_t>(image_shape[0].get_length()) / groups;
const size_t IH = static_cast<size_t>(image_shape[2].get_length());
const size_t IW = static_cast<size_t>(image_shape[3].get_length());
OC = groups;
auto img_shape_const = register_new_node<ov::op::v0::Constant>(
ov::element::i64, ov::Shape{4},
std::vector<int64_t>{static_cast<int64_t>(N), static_cast<int64_t>(groups), static_cast<int64_t>(IH),
static_cast<int64_t>(IW)});
ov::Output<Node> image_reshaped =
register_new_node<ov::op::v1::Reshape>(image_input, img_shape_const, false);
const ov::Shape conv_w_shape = {groups, 1, 1, KH, KW};
ov::Output<Node> weight_input;
if (auto weight_const = ov::as_type_ptr<ov::op::v0::Constant>(w_trace.get_node_shared_ptr())) {
weight_input = register_new_node<ov::op::v0::Constant>(weight_const->get_element_type(), conv_w_shape,
weight_const->get_data_ptr());
} else {
auto shape_const = register_new_node<ov::op::v0::Constant>(
ov::element::i64, ov::Shape{5},
std::vector<int64_t>{static_cast<int64_t>(groups), 1, 1, static_cast<int64_t>(KH),
static_cast<int64_t>(KW)});
weight_input = register_new_node<ov::op::v1::Reshape>(w_trace, shape_const, false);
}
if (weight_input.get_element_type() != image_reshaped.get_element_type()) {
weight_input = register_new_node<ov::op::v0::Convert>(weight_input, image_reshaped.get_element_type());
}
conv_out = register_new_node<ov::op::v1::GroupConvolution>(
image_reshaped, weight_input, eip->get_strides(), ov::CoordinateDiff{pb[2], pb[3]},
ov::CoordinateDiff{pe[2], pe[3]}, ov::Strides{eip->get_rates()[0], eip->get_rates()[1]},
ov::op::PadType::EXPLICIT);
} else {
if ((ws.size() == 4 && ws[1] == IC && ws[2] == KH && ws[3] == KW) ||
(ws.size() == 3 && ws[1] == IC && ws[2] == KW) || (ws.size() == 2 && ws[1] == kernel_spatial_ic)) {
OC = ws[0];
} else if ((ws.size() == 4 && ws[0] == 1 && ws[2] == IC && ws[3] == KW) ||
(ws.size() == 2 && ws[0] == kernel_spatial_ic)) {
OC = ws[1];
} else if (ws.size() == 3 && ws[0] == 1 && ws[1] == IC && ws[2] == KW) {
OC = 1;
} else if (ws.size() == 4 && ws[3] == kernel_spatial_ic) {
OC = ws[2];
} else if (ws.size() == 4 && ws[2] == kernel_spatial_ic) {
OC = ws[3];
} else if (kernel_spatial_ic && ov::shape_size(ws) % kernel_spatial_ic == 0) {
OC = ov::shape_size(ws) / kernel_spatial_ic;
} else {
return false;
}
const ov::Shape conv_w_shape = {OC, IC, KH, KW};
ov::Output<Node> weight_input;
if (auto weight_const = ov::as_type_ptr<ov::op::v0::Constant>(w_trace.get_node_shared_ptr())) {
weight_input = register_new_node<ov::op::v0::Constant>(weight_const->get_element_type(), conv_w_shape,
weight_const->get_data_ptr());
} else {
auto shape_const = register_new_node<ov::op::v0::Constant>(
ov::element::i64, ov::Shape{4},
std::vector<int64_t>{static_cast<int64_t>(OC), static_cast<int64_t>(IC), static_cast<int64_t>(KH),
static_cast<int64_t>(KW)});
weight_input = register_new_node<ov::op::v1::Reshape>(w_trace, shape_const, false);
}
if (weight_input.get_element_type() != image_input.get_element_type()) {
weight_input = register_new_node<ov::op::v0::Convert>(weight_input, image_input.get_element_type());
}
conv_out = register_new_node<ov::op::v1::Convolution>(
image_input, weight_input, eip->get_strides(), ov::CoordinateDiff{pb[2], pb[3]},
ov::CoordinateDiff{pe[2], pe[3]}, ov::Strides{eip->get_rates()[0], eip->get_rates()[1]},
ov::op::PadType::EXPLICIT);
}
constexpr auto target_type = ov::element::f32;
if (conv_out.get_element_type() != target_type) {
conv_out = register_new_node<ov::op::v0::Convert>(conv_out, target_type);
}
std::shared_ptr<ov::op::v1::Add> add_node;
ov::Output<Node> bias_input;
auto try_fuse_bias = [&](const std::shared_ptr<Node> & n) {
auto add = ov::as_type_ptr<ov::op::v1::Add>(n);
if (!add) {
return false;
}
for (size_t i = 0; i < 2; ++i) {
if (ov::is_type<ov::op::v0::Constant>(add->get_input_node_shared_ptr(i))) {
bias_input = add->input_value(i);
add_node = add;
return true;
}
}
return false;
};
for (const auto & consumer : matmul_node->output(0).get_target_inputs()) {
auto n = consumer.get_node()->shared_from_this();
if (try_fuse_bias(n)) {
break;
}
if (ov::is_type<ov::op::v0::Convert>(n) || ov::is_type<ov::op::v1::Reshape>(n)) {
for (const auto & next : n->output(0).get_target_inputs()) {
if (try_fuse_bias(next.get_node()->shared_from_this())) {
break;
}
}
}
if (add_node) {
break;
}
}
ov::Output<Node> final_out = conv_out;
std::shared_ptr<Node> target_node = matmul_node;
if (add_node) {
ov::Output<Node> bias = bias_input;
if (bias.get_element_type() != target_type) {
bias = register_new_node<ov::op::v0::Convert>(bias, target_type);
}
auto bias_shape = register_new_node<ov::op::v0::Constant>(
ov::element::i64, ov::Shape{4}, std::vector<int64_t>{1, static_cast<int64_t>(OC), 1, 1});
bias = register_new_node<ov::op::v1::Reshape>(bias, bias_shape, false);
final_out = register_new_node<ov::op::v1::Add>(conv_out, bias);
target_node = add_node;
}
if (!is_depthwise) {
auto perm = register_new_node<ov::op::v0::Constant>(
ov::element::i64, ov::Shape{4},
weight_is_in0 ? std::vector<int64_t>{1, 0, 2, 3} : std::vector<int64_t>{0, 2, 3, 1});
final_out = register_new_node<ov::op::v1::Transpose>(final_out, perm);
}
auto orig_shape = target_node->get_output_partial_shape(0);
if (orig_shape.is_static() && final_out.get_partial_shape().is_static()) {
if (ov::shape_size(orig_shape.to_shape()) != ov::shape_size(final_out.get_shape())) {
return false;
}
}
if (orig_shape.is_static() && final_out.get_partial_shape() != orig_shape) {
auto shape_const = register_new_node<ov::op::v0::Constant>(ov::element::i64, ov::Shape{orig_shape.size()},
orig_shape.to_shape());
final_out = register_new_node<ov::op::v1::Reshape>(final_out, shape_const, false);
}
auto orig_type = target_node->get_output_element_type(0);
if (final_out.get_element_type() != orig_type) {
final_out = register_new_node<ov::op::v0::Convert>(final_out, orig_type);
}
final_out.get_node_shared_ptr()->set_friendly_name(target_node->get_friendly_name());
ov::copy_runtime_info(m.get_matched_nodes(), final_out.get_node_shared_ptr());
ov::replace_node(target_node, final_out.get_node_shared_ptr());
return true;
};
register_matcher(std::make_shared<opp::Matcher>(m_matmul, "ov::frontend::ggml::pass::FuseToConv"), callback);
}
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,17 @@
#include "openvino/pass/matcher_pass.hpp"
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
class FuseToConv : public ov::pass::MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("ov::frontend::ggml::pass::FuseToConv")
FuseToConv();
};
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,60 @@
#include "fuse_to_sdpa.h"
#include <openvino/core/graph_util.hpp>
#include <openvino/core/rt_info.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/scaled_dot_product_attention.hpp>
#include <openvino/op/softmax.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/pass/pattern/op/label.hpp>
#include <openvino/pass/pattern/op/pattern.hpp>
#include <openvino/pass/pattern/op/wrap_type.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
FuseToSDPA::FuseToSDPA() {
// Not maintained since FLASH_ATTN_EXT has replaced this pattern
const auto m_k = ov::pass::pattern::any_input();
const auto m_q = ov::pass::pattern::any_input();
const auto m_qk = ov::pass::pattern::wrap_type<ov::op::v0::MatMul>({m_q, m_k});
const auto m_qk_f32 = ov::pass::pattern::wrap_type<ov::op::v0::Convert>({m_qk});
const auto m_scale = ov::pass::pattern::any_input();
const auto m_scaled_qk = ov::pass::pattern::wrap_type<ov::op::v1::Multiply>({m_qk_f32, m_scale});
const auto m_mask = ov::pass::pattern::any_input();
const auto m_masked_qk = ov::pass::pattern::wrap_type<ov::op::v1::Add>({m_scaled_qk, m_mask});
const auto m_softmax_qk = ov::pass::pattern::wrap_type<ov::op::v8::Softmax>({m_masked_qk});
const auto m_softmax_qk_f16 = ov::pass::pattern::wrap_type<ov::op::v0::Convert>({m_softmax_qk});
const auto m_v = ov::pass::pattern::any_input();
const auto m_qkv = ov::pass::pattern::wrap_type<ov::op::v0::MatMul>({m_softmax_qk_f16, m_v});
const auto callback = [=](ov::pass::pattern::Matcher & m) {
auto & pattern_to_output = m.get_pattern_value_map();
auto k = pattern_to_output[m_k];
auto q = pattern_to_output[m_q];
auto v = pattern_to_output[m_v];
auto mask = pattern_to_output[m_mask];
auto scale = pattern_to_output[m_scale];
auto mask_f16 = register_new_node<ov::op::v0::Convert>(mask, ov::element::f16);
auto scale_f16 = register_new_node<ov::op::v0::Convert>(scale, ov::element::f16);
auto sdpa = std::make_shared<ov::op::v13::ScaledDotProductAttention>(q, k, v, mask_f16, scale_f16, false);
ov::replace_node(m.get_match_root(), sdpa);
ov::copy_runtime_info(m.get_matched_nodes(), sdpa);
return true;
};
register_matcher(std::make_shared<ov::pass::pattern::Matcher>(m_qkv, "ov::frontend::ggml::pass::FuseToSDPA"),
callback);
}
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,17 @@
#include "openvino/pass/matcher_pass.hpp"
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
class FuseToSDPA : public ov::pass::MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("ov::frontend::ggml::pass::FuseToSDPA")
FuseToSDPA();
};
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,114 @@
#include "kv_state_seq_axis.h"
#include <memory>
#include <openvino/core/graph_util.hpp>
#include <openvino/op/assign.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/read_value.hpp>
#include <openvino/op/transpose.hpp>
#include <vector>
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
namespace {
const std::vector<int64_t> & seq_axis_perm() {
// [1, seq, n_heads_kv, head_size] <-> [1, n_heads_kv, seq, head_size]
static const std::vector<int64_t> perm{0, 2, 1, 3};
return perm;
}
// True when the state still has the frontend's stateful KV layout, so the sequence axis
// can be moved: rank 4, batch and both head dims static, and seq the only dynamic dim,
// at dim 1. Any KV head count is fine. With a single head the rewrite is pure metadata
// ([1, seq, 1, head] and [1, 1, seq, head] are the same memory); with several heads it
// also drops the reader-side transpose of the whole accumulated state, which is where
// most of the gain comes from at depth.
bool can_move_seq_axis(const ov::PartialShape & shape) {
return shape.rank().is_static() && shape.rank().get_length() == 4 && shape[0].is_static() &&
shape[1].is_dynamic() && shape[2].is_static() && shape[3].is_static();
}
std::shared_ptr<ov::op::v0::Concat> match_kv_append(const std::shared_ptr<ov::op::v6::Assign> & assign) {
auto concat = ov::as_type_ptr<ov::op::v0::Concat>(assign->get_input_node_shared_ptr(0));
if (!concat || concat->get_input_size() != 2 || concat->get_axis() != 1) {
return nullptr;
}
auto read_value = ov::as_type_ptr<ov::op::v6::ReadValue>(concat->get_input_node_shared_ptr(0));
if (!read_value || read_value->get_variable() != assign->get_variable()) {
return nullptr;
}
if (!can_move_seq_axis(read_value->get_output_partial_shape(0))) {
return nullptr;
}
return concat;
}
} // namespace
bool KVStateSeqAxis::run_on_model(const std::shared_ptr<ov::Model> & model) {
std::vector<std::shared_ptr<ov::op::v6::Assign>> assigns;
for (const auto & op : model->get_ops()) {
if (auto assign = ov::as_type_ptr<ov::op::v6::Assign>(op)) {
assigns.push_back(assign);
}
}
bool changed = false;
for (const auto & assign : assigns) {
auto concat = match_kv_append(assign);
if (!concat) {
continue;
}
auto read_value = ov::as_type_ptr<ov::op::v6::ReadValue>(concat->get_input_node_shared_ptr(0));
auto variable = read_value->get_variable();
auto info = variable->get_info();
const auto & shape = info.data_shape;
info.data_shape = ov::PartialShape{shape[0], shape[2], shape[1], shape[3]};
variable->update(info);
read_value->validate_and_infer_types();
auto readers = concat->output(0).get_target_inputs();
auto new_rows = concat->input_value(1);
auto perm_in = ov::op::v0::Constant::create(ov::element::i64, {4}, seq_axis_perm());
concat->set_argument(1, std::make_shared<ov::op::v1::Transpose>(new_rows, perm_in));
concat->set_axis(2);
concat->validate_and_infer_types();
// Readers still expect seq at dim 1. A reader that is itself the inverse
// Transpose wanted seq at dim 2 all along, so drop it; give anything else the
// inverse Transpose so its input is unchanged.
for (const auto & reader : readers) {
auto * node = reader.get_node();
if (ov::is_type<ov::op::v6::Assign>(node)) {
continue;
}
bool dropped = false;
if (auto * transpose = ov::as_type<ov::op::v1::Transpose>(node)) {
auto order = ov::as_type_ptr<ov::op::v0::Constant>(transpose->get_input_node_shared_ptr(1));
if (order && order->cast_vector<int64_t>() == seq_axis_perm()) {
ov::replace_output_update_name(transpose->output(0), concat->output(0));
dropped = true;
}
}
if (!dropped) {
auto perm_out = ov::op::v0::Constant::create(ov::element::i64, {4}, seq_axis_perm());
reader.replace_source_output(std::make_shared<ov::op::v1::Transpose>(concat->output(0), perm_out));
}
}
changed = true;
}
return changed;
}
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,24 @@
#include "openvino/pass/pass.hpp"
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
// Moves the sequence axis of the stateful KV cache from dim 1 to dim 2, i.e. from
// [1, seq, n_heads_kv, head_size] to [1, n_heads_kv, seq, head_size], and updates the
// Concat that appends to it. Two wins: the GPU plugin only appends new tokens in place
// when the growing axis is a spatial axis, and the reader no longer has to transpose the
// whole accumulated state every token (that cost grows with context length, so it is the
// larger win at depth for a model with several KV heads). Only rewrites states that still
// match the frontend layout, so it no-ops if that layout ever changes.
class KVStateSeqAxis : public ov::pass::ModelPass {
public:
OPENVINO_MODEL_PASS_RTTI("ov::frontend::ggml::pass::KVStateSeqAxis")
bool run_on_model(const std::shared_ptr<ov::Model> & model) override;
};
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,29 @@
#pragma once
#include "mark_decompression_convert_constant_folding.h"
#include "openvino/core/visibility.hpp"
#include "openvino/pass/matcher_pass.hpp"
#ifdef OPENVINO_STATIC_LIBRARY
# define TRANSFORMATIONS_API
#else
# ifdef IMPLEMENT_OPENVINO_API
# define TRANSFORMATIONS_API OPENVINO_CORE_EXPORTS
# else
# define TRANSFORMATIONS_API OPENVINO_CORE_IMPORTS
# endif // IMPLEMENT_OPENVINO_API
#endif // OPENVINO_STATIC_LIBRARY
namespace ov {
namespace pass {
class TRANSFORMATIONS_API MarkCompressedFloatConstants;
} // namespace pass
} // namespace ov
class ov::pass::MarkCompressedFloatConstants : public MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("MarkCompressedFloatConstants")
MarkCompressedFloatConstants();
};
@@ -0,0 +1,44 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
// Local mirror of OpenVINO's ov::pass::MarkDequantization pass declaration.
//
// The pass body is provided by the linked libopenvino.so; only the declaration is needed here so
// we can register it directly in our own TranslateSession::apply_transformations (same approach as
// MarkCompressedFloatConstants's local mirror in mark_decompression_convert_constant_folding.h). This
// lets us mark our GatherMatmul dequantization chain with disable_constant_folding regardless of the
// CPU/GPU plugin's own is_decompression_multiply() consumer allowlist.
// The class layout must stay in sync with
// openvino/src/common/transformations/include/transformations/low_precision/mark_dequantization_subgraph.hpp
#pragma once
#include "openvino/core/type/element_type.hpp"
#include "openvino/core/visibility.hpp"
#include "openvino/pass/matcher_pass.hpp"
#ifdef OPENVINO_STATIC_LIBRARY
# define TRANSFORMATIONS_API
#else
# ifdef IMPLEMENT_OPENVINO_API
# define TRANSFORMATIONS_API OPENVINO_CORE_EXPORTS
# else
# define TRANSFORMATIONS_API OPENVINO_CORE_IMPORTS
# endif // IMPLEMENT_OPENVINO_API
#endif // OPENVINO_STATIC_LIBRARY
namespace ov {
namespace pass {
class TRANSFORMATIONS_API MarkDequantization;
} // namespace pass
} // namespace ov
class ov::pass::MarkDequantization : public MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("MarkDequantization")
explicit MarkDequantization(const element::TypeVector & precisions,
bool fold_subtract_const = false,
bool fold_multiply_const = true);
};
@@ -0,0 +1,59 @@
#include "squeeze_matmul.h"
#include <openvino/core/graph_util.hpp>
#include <openvino/core/rt_info.hpp>
#include <openvino/core/type.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <openvino/pass/pattern/op/label.hpp>
#include <openvino/pass/pattern/op/pattern.hpp>
#include <openvino/pass/pattern/op/wrap_type.hpp>
namespace opp = ov::pass::pattern;
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
// For quantized models, NPUW expects the activation to be 3d in DQ(DynamicQuantization) opt, e.g. DQMatMulGQ2i
SqueezeMatmul::SqueezeMatmul() {
auto m_act = opp::any_input();
auto m_wei = opp::any_input();
auto m_matmul = opp::wrap_type<ov::op::v0::MatMul>({m_act, m_wei});
const auto callback = [=](ov::pass::pattern::Matcher & m) {
const auto & pattern_map = m.get_pattern_value_map();
auto matmul_node =
ov::as_type_ptr<ov::op::v0::MatMul>(pattern_map.at(m_matmul).get_node_shared_ptr());
auto act = pattern_map.at(m_act);
auto wei = pattern_map.at(m_wei);
auto act_shape = act.get_partial_shape();
auto wei_shape = wei.get_partial_shape();
if (act_shape.rank().is_dynamic() || wei_shape.rank().is_dynamic()) {
return false;
}
if (act_shape.rank().get_length() == 4 && wei_shape.rank().get_length() == 2) {
auto axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {0});
auto squeezed_act = std::make_shared<ov::op::v0::Squeeze>(act, axis);
auto new_matmul = std::make_shared<ov::op::v0::MatMul>(squeezed_act, wei, matmul_node->get_transpose_a(),
matmul_node->get_transpose_b());
auto unsqueezed_output = std::make_shared<ov::op::v0::Unsqueeze>(new_matmul, axis);
unsqueezed_output->set_friendly_name(matmul_node->get_friendly_name());
ov::copy_runtime_info(matmul_node, {squeezed_act, new_matmul, unsqueezed_output});
ov::replace_node(matmul_node, unsqueezed_output);
return true;
}
return false;
};
register_matcher(std::make_shared<ov::pass::pattern::Matcher>(m_matmul, "ov::frontend::ggml::pass::SqueezeMatmul"),
callback);
}
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,17 @@
#include "openvino/pass/matcher_pass.hpp"
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
class SqueezeMatmul : public ov::pass::MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("ov::frontend::ggml::pass::SqueezeMatmul")
SqueezeMatmul();
};
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,41 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#pragma once
#include <openvino/core/core_visibility.hpp>
#include <openvino/core/node.hpp>
#include <openvino/core/runtime_attribute.hpp>
namespace ov {
/**
* @brief Holds weightless caching attributes of a single constant.
*
* WeightlessCacheAttribute class represents runtime info attribute that holds
* the values of original size of the constant in bytes and the binary offset of the
* constant's data in the weights file used by the weightless caching mechanism. It's
* not copyable in case the data was changed (the original node was replaced by a new
* one produced during the tranformation pipeline) - in that case weightless caching
* can't be used for that constant.
*/
class OPENVINO_API WeightlessCacheAttribute : public RuntimeAttribute {
public:
OPENVINO_RTTI("WeightlessCacheAttribute", "0", RuntimeAttribute)
WeightlessCacheAttribute() = delete;
WeightlessCacheAttribute(size_t original_size, size_t bin_offset, ov::element::Type original_dtype)
: original_size(original_size),
bin_offset(bin_offset),
original_dtype(original_dtype) {}
bool is_copyable() const override;
size_t original_size;
size_t bin_offset;
ov::element::Type original_dtype;
};
} // namespace ov
@@ -0,0 +1,546 @@
#include "translate_session.h"
#include "ggml-impl.h"
#include "ggml-openvino/ggml-openvino-extra.h"
#include "ggml-openvino/openvino/node_context.h"
#include "ggml-openvino/openvino/utils.h"
#include "input_model.h"
#include "pass/fuse_argsort_topk.h"
#include "pass/fuse_moe_router.h"
#include "pass/align_eltwise_ranks.h"
#include "pass/fuse_moe_compressed.h"
#include "pass/fuse_to_conv.h"
#include "pass/kv_state_seq_axis.h"
#include "pass/mark_decompression_convert_constant_folding.h"
#include "pass/mark_dequantization_subgraph.h"
#include "pass/squeeze_matmul.h"
#include "rt_info/weightless_caching_attributes.hpp"
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <map>
#include <memory>
#include <openvino/core/node.hpp>
#include <openvino/core/preprocess/pre_post_process.hpp>
#include <openvino/core/shape.hpp>
#include <openvino/core/type.hpp>
#include <openvino/core/type/element_type.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/convert_like.hpp>
#include <openvino/op/cos.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/greater_eq.hpp>
#include <openvino/op/less.hpp>
#include <openvino/op/logical_and.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/parameter.hpp>
#include <openvino/op/range.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/result.hpp>
#include <openvino/op/select.hpp>
#include <openvino/op/sin.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/strided_slice.hpp>
#include <openvino/op/subtract.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <openvino/pass/constant_folding.hpp>
#include <openvino/pass/make_stateful.hpp>
#include <limits>
#include <sstream>
namespace ov {
namespace frontend {
namespace ggml {
using namespace ov::op;
namespace {
std::shared_ptr<ov::op::v0::Parameter> create_parameter(const std::string & name,
const ModelInputInfo & input_info) {
auto param_node = std::make_shared<ov::op::v0::Parameter>(input_info.type, input_info.shape);
param_node->set_friendly_name(name);
param_node->output(0).get_tensor().set_names({name});
return param_node;
}
std::shared_ptr<ov::Node> create_extra_input(const std::string & name, const ModelExtraInputInfo & input_info) {
if (input_info.is_parameter) {
auto param_node = std::make_shared<ov::op::v0::Parameter>(input_info.type, input_info.shape);
param_node->set_friendly_name(name);
param_node->output(0).get_tensor().set_names({name});
return param_node;
}
auto constant = std::make_shared<ov::op::v0::Constant>(input_info.type, input_info.shape,
std::vector<int64_t>{input_info.value});
constant->set_friendly_name(name);
return constant;
}
ov::pass::MakeStateful::ParamResPairs get_kv_param_res_pairs(
const std::shared_ptr<ov::Model> & model,
const std::map<std::string, std::string> & kv_param_res_names) {
ov::pass::MakeStateful::ParamResPairs pairs;
const auto & params = model->get_parameters();
const auto & results = model->get_results();
for (const auto & param_res : kv_param_res_names) {
const auto & param_name = param_res.first;
const auto & res_name = param_res.second;
auto param_it = std::find_if(params.begin(), params.end(), [&](const std::shared_ptr<v0::Parameter> & node) {
return node->get_friendly_name() == param_name;
});
OPENVINO_ASSERT(param_it != params.end(), "The tensor name ", param_name,
" is not associated with any of "
"Parameters in the network.");
auto res_it = std::find_if(results.begin(), results.end(), [&](const std::shared_ptr<v0::Result> & node) {
return node->get_friendly_name() == res_name;
});
OPENVINO_ASSERT(res_it != results.end(), "The tensor name ", res_name,
" is not associated with any of "
"Results in the network.");
std::shared_ptr<ov::op::v0::Parameter> param = *param_it;
std::shared_ptr<ov::op::v0::Result> res = *res_it;
pairs.emplace_back(param, res);
}
return pairs;
}
void add_sliced_mask_stateful(TensorMap & tensor_map, bool imrope) {
auto create_sliced_mask = [&](const std::string & mask_name, const std::string & sliced_name) {
if ((tensor_map.find(mask_name) != tensor_map.end()) &&
(tensor_map.find("token_len_per_seq") != tensor_map.end()) &&
(tensor_map.find("inp_pos") != tensor_map.end())) {
auto token_len_per_seq = tensor_map.at("token_len_per_seq").get_node_shared_ptr();
auto mask = tensor_map.at(mask_name).get_node_shared_ptr();
std::shared_ptr<ov::Node> mask_sliced = mask;
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto three = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
auto neg_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto step = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto axes = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto inp_pos = tensor_map.at("inp_pos").get_node_shared_ptr();
// IMROPE's fourth position plane is zero for text; use the token's first plane.
ov::Output<ov::Node> last_index = neg_one;
if (imrope) {
last_index = std::make_shared<ov::op::v1::Add>(token_len_per_seq, neg_one);
}
auto last_inp_pos = std::make_shared<ov::op::v8::Gather>(inp_pos, last_index, three);
auto last_inp_pos_1d = std::make_shared<ov::op::v1::Reshape>(
last_inp_pos, ov::op::v0::Constant::create(ov::element::i64, {1}, {1}), false);
auto last_inp_pos_cvt = std::make_shared<ov::op::v0::Convert>(last_inp_pos_1d, ov::element::i64);
auto last_inp_pos_inc = std::make_shared<ov::op::v1::Add>(last_inp_pos_cvt, one);
mask_sliced = std::make_shared<ov::op::v8::Slice>(mask, zero, last_inp_pos_inc, step, axes);
mask_sliced = std::make_shared<ov::op::v0::Convert>(mask_sliced, ov::element::f16);
mask_sliced->set_friendly_name(sliced_name);
tensor_map.insert({sliced_name, mask_sliced->output(0)});
}
};
create_sliced_mask("self_kq_mask", "KQ_mask_sliced");
create_sliced_mask("KQ_mask", "KQ_mask_sliced");
create_sliced_mask("self_kq_mask_swa", "KQ_mask_swa_sliced");
}
// Rebuild the sliding-window mask from absolute positions.
// ggml caps self_kq_mask_swa at the size of its own SWA cache, but the stateful KV state is
// Concat-appended and grows without bound, so past that cap the two disagree on length and the
// mask add fails. A pure-Concat state is ordered by position, so positions can rebuild the mask.
// swa_window holds the real n_swa, read back from the ggml mask in ggml-decoder.cpp.
// No-op when the graph has no SWA mask, or when the window could not be read back.
void add_position_mask_stateful_swa(TensorMap & tensor_map) {
if (tensor_map.find("self_kq_mask_swa") == tensor_map.end() || tensor_map.find("inp_pos") == tensor_map.end() ||
tensor_map.find("swa_window") == tensor_map.end()) {
return;
}
auto inp_pos = tensor_map.at("inp_pos").get_node_shared_ptr();
auto zero_i64 = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto one_i64 = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto three = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
auto neg_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto query_pos = std::make_shared<ov::op::v0::Convert>(inp_pos, ov::element::i64);
auto query_pos_1d = std::make_shared<ov::op::v1::Reshape>(
query_pos, ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}), false);
auto last_pos = std::make_shared<ov::op::v8::Gather>(inp_pos, neg_one, three);
auto last_pos_1d = std::make_shared<ov::op::v1::Reshape>(last_pos, one_i64, false);
auto last_pos_cvt = std::make_shared<ov::op::v0::Convert>(last_pos_1d, ov::element::i64);
auto total_len = std::make_shared<ov::op::v1::Add>(last_pos_cvt, one_i64);
auto total_len_scalar = std::make_shared<ov::op::v0::Squeeze>(total_len);
auto cached_pos = std::make_shared<ov::op::v4::Range>(
ov::op::v0::Constant::create(ov::element::i64, {}, {0}), total_len_scalar,
ov::op::v0::Constant::create(ov::element::i64, {}, {1}), ov::element::i64);
auto query_col = std::make_shared<ov::op::v1::Reshape>(
query_pos_1d, ov::op::v0::Constant::create(ov::element::i64, {2}, {-1, 1}), false);
auto cached_row = std::make_shared<ov::op::v1::Reshape>(
cached_pos, ov::op::v0::Constant::create(ov::element::i64, {2}, {1, -1}), false);
auto diff = std::make_shared<ov::op::v1::Subtract>(query_col, cached_row);
auto swa_window = tensor_map.at("swa_window").get_node_shared_ptr();
auto window = std::make_shared<ov::op::v0::Convert>(swa_window, ov::element::i64);
auto causal_ok = std::make_shared<ov::op::v1::GreaterEqual>(diff, zero_i64);
auto window_ok = std::make_shared<ov::op::v1::Less>(diff, window);
auto keep = std::make_shared<ov::op::v1::LogicalAnd>(causal_ok, window_ok);
auto zero_f = ov::op::v0::Constant::create(ov::element::f32, {}, {0.0f});
auto neg_inf_f = ov::op::v0::Constant::create(ov::element::f32, {}, {-std::numeric_limits<float>::infinity()});
std::shared_ptr<ov::Node> mask = std::make_shared<ov::op::v1::Select>(keep, zero_f, neg_inf_f);
auto batch_axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
mask = std::make_shared<ov::op::v0::Unsqueeze>(mask, batch_axis);
mask = std::make_shared<ov::op::v0::Unsqueeze>(mask, batch_axis);
mask = std::make_shared<ov::op::v0::Convert>(mask, ov::element::f16);
mask->set_friendly_name("KQ_mask_swa_sliced");
tensor_map["KQ_mask_swa_sliced"] = mask->output(0);
}
void add_rope_sin_cos(TensorMap & tensor_map, GgmlDecoder & ggml_model_decoder) {
// When ROPE ops in the graph have divergent op_params (e.g. gemma4's mixed
// SWA/non-SWA layers with different n_dims or freq_base), a shared sin/cos
// precompute cannot broadcast across every ROPE use. Skip it here and let
// translate_rope() build sin/cos per-op from its own op_params.
if (ggml_model_decoder.has_mixed_rope_params()) {
return;
}
int32_t * rope_params = ggml_model_decoder.get_rope_params();
if (tensor_map.find("inp_pos") == tensor_map.end() || rope_params == nullptr) {
return;
}
auto inp_pos = tensor_map.at("inp_pos").get_node_shared_ptr();
std::shared_ptr<ov::Node> rope_freqs_weight;
if (tensor_map.find("rope_freqs.weight") != tensor_map.end()) {
rope_freqs_weight = tensor_map.at("rope_freqs.weight").get_node_shared_ptr();
}
auto sin_cos = make_sin_cos(rope_params, inp_pos, rope_freqs_weight);
auto sin_theta = sin_cos.first;
auto cos_theta = sin_cos.second;
cos_theta.get_node_shared_ptr()->set_friendly_name("rope_cos");
sin_theta.get_node_shared_ptr()->set_friendly_name("rope_sin");
tensor_map.insert({"rope_cos", cos_theta});
tensor_map.insert({"rope_sin", sin_theta});
}
// Create common patterns
void preprocess(TensorMap & tensor_map, GgmlDecoder & ggml_model_decoder) {
if (ggml_model_decoder.is_stateful()) {
add_sliced_mask_stateful(tensor_map, ggml_model_decoder.get_rope_params()[2] == GGML_ROPE_TYPE_IMROPE);
add_position_mask_stateful_swa(tensor_map);
}
// This optimization is error-prone
// add_rope_sin_cos(tensor_map, ggml_model_decoder);
}
} // namespace
TranslateSession::TranslateSession(const frontend::InputModel::Ptr & input_model,
const std::unordered_map<std::string, CreatorFunction> & translator_map,
bool naive) :
m_input_model(input_model),
m_translator_map(translator_map),
m_ov_model(nullptr),
m_naive(naive) {}
std::shared_ptr<Model> TranslateSession::get_converted_model() {
if (m_ov_model) {
return m_ov_model;
}
m_ov_model = translate_graph(m_input_model);
return m_ov_model;
}
std::shared_ptr<Model> TranslateSession::translate_graph(const frontend::InputModel::Ptr & input_model) {
ov::ParameterVector params;
ov::ResultVector results;
auto tensor_map = std::make_shared<TensorMap>();
std::shared_ptr<Model> resulting_model;
const auto & ggml_model = ov::as_type_ptr<InputModel>(input_model);
std::shared_ptr<GgmlDecoder> ggml_model_decoder = ggml_model->get_model_decoder();
for (const auto & it : ggml_model_decoder->get_model_inputs()) {
auto param_node = create_parameter(it.first, it.second);
params.push_back(param_node);
(*tensor_map)[it.first] = param_node;
}
for (const auto & it : ggml_model_decoder->get_model_extra_inputs()) {
auto input_node = create_extra_input(it.first, it.second);
if (it.second.is_parameter) {
params.push_back(ov::as_type_ptr<ov::op::v0::Parameter>(input_node));
}
(*tensor_map)[it.first] = input_node;
}
for (const auto & it : ggml_model_decoder->get_model_weights()) {
(*tensor_map)[it.first] = it.second;
}
auto translate_node = [&](const std::shared_ptr<GgmlDecoder> & decoder, int node_idx) {
auto operation_type = decoder->get_op_type(node_idx);
if (operation_type == "GGML_OP_NONE") {
return ov::OutputVector{};
}
const auto & node_output_names = decoder->get_output_names(node_idx);
if (operation_type == "GGML_OP_VIEW" && decoder->get_op_case(node_idx) == 2 && node_output_names.size() == 1) {
auto direct_output = tensor_map->find(node_output_names[0]);
if (direct_output != tensor_map->end()) {
// GDN publishes its native attention/state outputs under the two GGML VIEW names.
// Keep those mappings instead of rebuilding slices of a packed temporary.
return ov::OutputVector{direct_output->second};
}
}
auto it = m_translator_map.find(operation_type);
FRONT_END_OP_CONVERSION_CHECK(it != m_translator_map.end(), "Translation for operation type ", operation_type,
" is not implemented.");
NodeContext node_context(decoder, tensor_map, node_idx, this);
ov::OutputVector converted_outputs = it->second(node_context);
FRONT_END_OP_CONVERSION_CHECK(node_output_names.size() == converted_outputs.size(), "Number of ",
operation_type, " outputs greater than number of converted outputs, which are ",
node_output_names.size(), " and ", converted_outputs.size(), " respectively.");
for (size_t i = 0; i < node_output_names.size(); ++i) {
auto output_name = node_output_names[i];
if (i < converted_outputs.size() && converted_outputs[i].get_node_shared_ptr() != nullptr) {
(*tensor_map)[output_name] = converted_outputs[i];
}
}
return converted_outputs;
};
// To handle cases like this
// 3: [ 18432, 1, 1, 1] RESHAPE cache_r_l0 (reshaped)#3
// [ 18432, 1, 1, 1] 0: NONE cache_r_l0
// 4: [ 0, 1, 1, 1] VIEW cache_r_l0 (reshaped) (view)#4
// [ 18432, 1, 1, 1] 0: RESHAPE cache_r_l0 (reshaped)#3
// 5: [ 0, 1, 1, 1] SCALE cache_r_l0 (reshaped) (view) (view)#5
// [ 0, 1, 1, 1] 0: VIEW cache_r_l0 (reshaped) (view)#4
// 6: [ 1, 1, 1, 1] VIEW (view)#6
// [ 1, 1, 1, 1] 0: NONE leaf_5
// 7: [ 18432, 1, 1, 1] GET_ROWS conv_states-0#7
// [ 18432, 1, 1, 1] 0: RESHAPE cache_r_l0 (reshaped)#3
// [ 1, 1, 1, 1] 1: VIEW (view)#6
// The scale is in-place which modifies cache_r_l0 (reshaped)#3
// The translation of scale overwrites cache_r in the tensor_map,
// but we also need to overwrite the old cache_r_l0 (reshaped)#3
auto refresh_inplace_aliases = [&](const std::shared_ptr<GgmlDecoder> & decoder, int inplace_node_idx,
const std::string & view_src_name) {
for (int node_idx = 0; node_idx < inplace_node_idx; node_idx++) {
if (decoder->is_view_like_alias_of(node_idx, view_src_name)) {
translate_node(decoder, node_idx);
}
}
};
auto node_visitor = [&](const std::shared_ptr<GgmlDecoder> & decoder, int node_idx) {
auto converted_outputs = translate_node(decoder, node_idx);
if (converted_outputs.empty()) {
return;
}
const auto inplace_src = decoder->get_inplace_op_src(node_idx);
if (inplace_src.empty()) {
return;
}
if (converted_outputs[0].get_node_shared_ptr() != nullptr) {
(*tensor_map)[inplace_src] = converted_outputs[0];
}
refresh_inplace_aliases(decoder, node_idx, inplace_src);
};
if (!m_naive) {
preprocess(*tensor_map, *ggml_model_decoder);
}
ggml_model_decoder->visit_subgraph(node_visitor);
for (const auto & name : ggml_model_decoder->get_model_output_names()) {
FRONT_END_GENERAL_CHECK(tensor_map->find(name) != tensor_map->end(),
"Output name not found in tensor map: ", name);
auto result = std::make_shared<v0::Result>(tensor_map->at(name));
result->set_friendly_name(name);
results.push_back(result);
}
// Debug-only hook: GGML_OPENVINO_DEBUG_NODE=<name1>,<name2>,... adds extra
// Result nodes for arbitrary intermediate tensors (looked up by name in
// tensor_map), on top of the real model outputs above. These debug
// Results are deliberately NOT added to ggml_decoder's model outputs, so
// the caller (ov_graph_compute_dynamic in utils.cpp) will not bind them
// to any ggml tensor buffer -- OpenVINO allocates its own tensor for
// them. This avoids the risk of reading a ggml buffer that has since
// been overwritten by a later in-place op (ggml aggressively reuses
// buffers), which can happen if trying to inspect an intermediate value
// via GGML_OPENVINO_DEBUG_OUTPUT by hacking it into a real output.
//
// tensor_map keys are usually the plain ggml tensor name (e.g. "embd"),
// but tensors that are recomputed multiple times in the same cgraph
// (GGML_TENSOR_FLAG_COMPUTE) are disambiguated with a "#<hash>" suffix
// (e.g. "cache_k_l0#4853", see get_tensor_ov_name()) which is not
// predictable ahead of time. To keep the env var usable, a requested
// name is matched either exactly, or as the "name" part before "#" of a
// suffixed key (first match wins; ambiguous requests should include the
// full "name#hash" form seen in a previous run's log/dump).
if (const char * debug_nodes = ggml_openvino_getenv_str("GGML_OPENVINO_DEBUG_NODE")) {
std::stringstream ss(debug_nodes);
std::string name;
while (std::getline(ss, name, ',')) {
auto it = tensor_map->find(name);
if (it == tensor_map->end()) {
it = std::find_if(tensor_map->begin(), tensor_map->end(), [&](const auto & entry) {
return entry.first.compare(0, name.size(), name) == 0 && entry.first.size() > name.size() &&
entry.first[name.size()] == '#';
});
}
if (it == tensor_map->end()) {
GGML_LOG_WARN("GGML_OPENVINO_DEBUG_NODE: node '%s' not found in tensor map, skipping\n", name.c_str());
continue;
}
auto result = std::make_shared<v0::Result>(it->second);
result->set_friendly_name("__debug_" + it->first);
results.push_back(result);
}
}
ov::ParameterVector used_params;
for (const auto & param : params) {
if (!param->output(0).get_target_inputs().empty()) {
used_params.push_back(param);
}
}
// if (auto diff = params.size() - used_params.size()) {
// GGML_LOG_INFO("%zu parameters are not used in the model.", diff);
// }
resulting_model = std::make_shared<Model>(results, used_params);
apply_transformations(resulting_model);
// Set WeightlessCacheAttribute on large constants to avoid unnecessary memory copies
// in the NPUW plugin. Without this attribute, NPUW's LazyTensor constructor
// (lazy_tensor.cpp, op::Const::Const) will memcpy every constant "in case export
// occurs", doubling memory usage per compile_model call.
//
// The bin_offset field serves as a unique key (not a real file offset) — this is
// the same convention the GPU plugin uses for non-IR models (see
// Plugin::set_weightless_cache_attributes in intel_gpu/src/plugin/plugin.cpp).
// Each constant must have a distinct bin_offset, otherwise GPU's weightless cache
// import will map multiple constants to the same data.
//
// Small constants (< 16 elements) are excluded since they may be introduced by
// optimization patterns and the overhead is negligible.
//
// Note: use shape_size() rather than byte_size()/element_type().size() - GatherMatmul's default
// bias is a Constant(element::dynamic, Shape{0}), whose element_type().size() is 0 and would
// divide by zero.
size_t offset = 0;
for (auto & node : resulting_model->get_ordered_ops()) {
if (auto cnst = ov::as_type_ptr<ov::op::v0::Constant>(node); cnst && ov::shape_size(cnst->get_shape()) >= 16) {
auto & rt_info = cnst->get_rt_info();
if (rt_info.find(ov::WeightlessCacheAttribute::get_type_info_static()) == rt_info.end()) {
rt_info[ov::WeightlessCacheAttribute::get_type_info_static()] =
ov::WeightlessCacheAttribute(cnst->get_byte_size(), offset++, cnst->get_element_type());
}
}
}
return resulting_model;
}
std::shared_ptr<Model> TranslateSession::apply_transformations(std::shared_ptr<Model> model) {
auto ggml_model_decoder = ov::as_type_ptr<InputModel>(m_input_model)->get_model_decoder();
{
ov::pass::Manager manager;
manager.set_per_pass_validation(true);
manager.register_pass<ov::pass::MarkCompressedFloatConstants>();
// Marks the Convert/Subtract/Multiply nodes of our GatherMatmul dequantization chain
// (make_int4_weights/make_int8_weights, for_gather_matmul=true) with disable_constant_folding,
// so it survives ConstantFolding regardless of whether the target plugin's own
// is_decompression_multiply() recognizes GatherMatmul as a valid consumer.
manager.register_pass<ov::pass::MarkDequantization>(
std::vector<ov::element::Type>{ov::element::u8, ov::element::i8, ov::element::u4, ov::element::i4});
manager.register_pass<pass::FuseToConv>();
manager.register_pass<pass::FuseArgsortTopK>();
// MOECompressed has no CPU plugin implementation, so enable it by default only on GPU.
// GGML_OPENVINO_MOE_OP=0 keeps the unfused GatherMatmul path for fallback/debugging.
if (ggml_openvino_is_gpu() &&
ggml_openvino_getenv_int("GGML_OPENVINO_MOE_OP", 1) != 0) {
manager.register_pass<pass::FuseMoeRouter>();
manager.register_pass<pass::FuseMoeCompressed>();
// Same fusion for models whose gate and up projections share one fused expert
// weight (gemma-4). It only matches that shape and only when the experts carry an
// integer zero point, so it is a no-op on the separate-gate/up models above.
manager.register_pass<pass::FuseMoeCompressedFusedGateUp>();
}
// Workaround for an OpenVINO GPU-plugin defect: an eltwise op whose operands differ in
// rank is computed wrongly once the plugin fuses it as a post-op into `rms`. gemma-4
// dense under stateful execution adds a rank-3 residual to the rank-4 norm output and
// decodes garbage on GPU while CPU is correct. Equalising the ranks is a no-op for
// NUMPY broadcasting and makes the fused path correct.
// Remove once the plugin guards that fusion. Opt out with
// GGML_OPENVINO_DISABLE_ELTWISE_RANK_ALIGN=1.
if (ggml_openvino_is_gpu() && !ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_ELTWISE_RANK_ALIGN")) {
manager.register_pass<pass::AlignEltwiseOperandRanks>();
}
if (ggml_model_decoder->is_stateful()) {
const auto kv_param_res_names = ggml_model_decoder->get_kv_param_res_names();
const auto kv_param_res_pairs = get_kv_param_res_pairs(model, kv_param_res_names);
manager.register_pass<ov::pass::MakeStateful>(kv_param_res_pairs);
// Must run after MakeStateful, which is what creates the ReadValue/Assign pairs.
if (!ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_KV_STATE_RELAYOUT")) {
manager.register_pass<pass::KVStateSeqAxis>();
}
}
if (ggml_model_decoder->is_static()) {
manager.register_pass<pass::SqueezeMatmul>();
}
manager.run_passes(model);
if (ggml_model_decoder->is_stateful()) {
ov::preprocess::PrePostProcessor ppp(model);
for (size_t i = 0; i < model->get_output_size(); i++) {
auto model_output_shape = model->output(i).get_partial_shape();
if (model_output_shape.rank().is_static() && model_output_shape.rank().get_length() == 3) {
ppp.output(i).postprocess().custom([](const ov::Output<ov::Node>& node) {
auto axes = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {0});
return std::make_shared<ov::op::v0::Unsqueeze>(node, axes);
});
}
}
model = ppp.build();
}
}
return model;
}
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,29 @@
#pragma once
#include "input_model.h"
#include "node_context.h"
namespace ov {
namespace frontend {
namespace ggml {
class TranslateSession {
public:
TranslateSession(const frontend::InputModel::Ptr & input_model,
const std::unordered_map<std::string, CreatorFunction> & translator_map,
bool naive = false);
std::shared_ptr<Model> get_converted_model();
std::shared_ptr<Model> translate_graph(const frontend::InputModel::Ptr & input_model);
private:
std::shared_ptr<Model> apply_transformations(std::shared_ptr<Model> model);
const frontend::InputModel::Ptr m_input_model;
const std::unordered_map<std::string, CreatorFunction> & m_translator_map;
std::shared_ptr<Model> m_ov_model;
bool m_naive;
};
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,987 @@
#include "utils.h"
#include <cmath>
#include <cstddef>
#include <ctime>
#include <memory>
#include <openvino/op/add.hpp>
#include <openvino/op/clamp.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/cos.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/maximum.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/sin.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/split.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/subtract.hpp>
#include <openvino/op/transpose.hpp>
#include <string>
namespace ov {
namespace frontend {
namespace ggml {
void num_inputs_check(const NodeContext & context, size_t min_inputs, size_t max_inputs) {
auto input_size = context.get_input_size();
FRONT_END_OP_CONVERSION_CHECK(input_size >= min_inputs, "Got less inputs than expected");
FRONT_END_OP_CONVERSION_CHECK(input_size <= max_inputs, "Got more inputs than expected");
}
int non_cont_dim(std::vector<size_t> ne, std::vector<size_t> nb) {
int dim = nb.size() - 1;
size_t bytes = nb[dim];
for (int i = dim; i > 0; i--) {
bytes *= ne[i];
if (bytes != nb[i - 1]) {
return i;
}
}
return 0;
}
std::shared_ptr<ov::Node> get_dimensions(const std::shared_ptr<ov::op::v3::ShapeOf> & shape,
const std::vector<int> & dims) {
using namespace ov::op;
const auto zero = v0::Constant::create(ov::element::i32, ov::Shape{}, {0});
const auto dims_const = v0::Constant::create(ov::element::i32, ov::Shape{dims.size()}, dims);
return std::make_shared<v8::Gather>(shape, dims_const, zero);
}
std::shared_ptr<ov::Node> get_dimensions(const std::shared_ptr<ov::Node> & node, const std::vector<int> & dims) {
return get_dimensions(std::make_shared<ov::op::v3::ShapeOf>(node), dims);
}
OutputVector rename_outputs_with_suffix(const OutputVector & outputs, const std::string & suffix) {
for (const auto & output : outputs) {
auto node = output.get_node_shared_ptr();
std::string name = node->get_friendly_name();
name += "_";
name += suffix;
node->set_friendly_name(name);
// Uncomment to dump every node's inferred shape (used to hunt down dynamic dims on NPU).
// std::cout << name << " " << output.get_partial_shape() << std::endl;
}
return outputs;
}
namespace {
ov::Output<ov::Node> rope_yarn_ramp_mix(int num_elements, const float corr_dims[2], float ext_factor) {
std::vector<float> dim_ids_vec(num_elements);
std::iota(dim_ids_vec.begin(), dim_ids_vec.end(), 0.0f);
auto dim_ids = ov::op::v0::Constant::create(ov::element::f32, Shape{1, 1, 1, (size_t) num_elements}, dim_ids_vec);
auto corr_low = ov::op::v0::Constant::create(ov::element::f32, Shape{1, 1, 1, 1}, {corr_dims[0]});
auto corr_high = ov::op::v0::Constant::create(ov::element::f32, Shape{1, 1, 1, 1}, {corr_dims[1]});
auto denom = std::make_shared<ov::op::v1::Maximum>(
std::make_shared<ov::op::v1::Subtract>(corr_high, corr_low),
ov::op::v0::Constant::create(ov::element::f32, Shape{1, 1, 1, 1}, {0.001f}));
auto ramp_y =
std::make_shared<ov::op::v1::Divide>(std::make_shared<ov::op::v1::Subtract>(dim_ids, corr_low), denom);
auto ramp_clamped = std::make_shared<ov::op::v0::Clamp>(ramp_y, 0.0f, 1.0f);
// rope_yarn_ramp returns (1 - clamp(y)), so invert before scaling
auto one = ov::op::v0::Constant::create(ov::element::f32, Shape{1, 1, 1, 1}, {1.0f});
auto ramp_inverted = std::make_shared<ov::op::v1::Subtract>(one, ramp_clamped);
auto ext_factor_node = ov::op::v0::Constant::create(ov::element::f32, Shape{}, {ext_factor});
auto ramp_mix = std::make_shared<ov::op::v1::Multiply>(ramp_inverted, ext_factor_node);
return ramp_mix;
}
float ggml_rope_yarn_corr_dim(int n_dims, int n_ctx_orig, float n_rot, float base) {
#ifndef M_PI
# define M_PI 3.14159265358979323846
#endif
return n_dims * logf(n_ctx_orig / (n_rot * 2 * (float) M_PI)) / (2 * logf(base));
}
void ggml_rope_yarn_corr_dims(int n_dims,
int n_ctx_orig,
float freq_base,
float beta_fast,
float beta_slow,
float dims[2]) {
float start = floorf(ggml_rope_yarn_corr_dim(n_dims, n_ctx_orig, beta_fast, freq_base));
float end = ceilf(ggml_rope_yarn_corr_dim(n_dims, n_ctx_orig, beta_slow, freq_base));
dims[0] = std::max(0.0f, start);
dims[1] = std::min(static_cast<float>(n_dims - 1), end);
}
} // namespace
std::pair<ov::Output<Node>, ov::Output<Node>> make_sin_cos(int32_t * rope_params,
std::shared_ptr<ov::Node> inp_pos,
std::shared_ptr<ov::Node> rope_freqs_weight,
int mode,
bool stateful,
int64_t head_dim) {
constexpr int TYPE_IMROPE = 2;
constexpr int TYPE_VISION = 3;
constexpr int TYPE_MROPE = 4;
const bool is_imrope = (mode == TYPE_IMROPE);
const bool is_vision = (mode == TYPE_VISION);
const bool is_mrope = (mode == TYPE_MROPE);
const bool is_multi = is_imrope || is_vision || is_mrope;
if (stateful) {
inp_pos =
std::make_shared<ov::op::v0::Squeeze>(inp_pos, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
inp_pos = std::make_shared<ov::op::v0::Convert>(inp_pos, ov::element::f32);
auto pos_perm =
std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{3}, std::vector<int64_t>{2, 1, 0});
inp_pos = std::make_shared<ov::op::v1::Transpose>(inp_pos, pos_perm);
} else if (is_multi) {
inp_pos = std::make_shared<ov::op::v0::Convert>(inp_pos, ov::element::f32);
auto pos_shape = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{5}, {0, 0, 0, 4, -1});
inp_pos = std::make_shared<ov::op::v1::Reshape>(inp_pos, pos_shape, true);
auto pos_transpose_shape =
std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{5}, std::vector<int64_t>{0, 1, 2, 4, 3});
inp_pos = std::make_shared<ov::op::v1::Transpose>(inp_pos, pos_transpose_shape);
} else {
inp_pos = std::make_shared<ov::op::v0::Convert>(inp_pos, ov::element::f32);
auto pos_perm =
std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{4}, std::vector<int64_t>{0, 3, 1, 2});
inp_pos = std::make_shared<ov::op::v1::Transpose>(inp_pos, pos_perm);
}
float freq_base;
float freq_scale;
float ext_factor;
float attn_factor;
float beta_fast;
float beta_slow;
const int n_dims = rope_params[1];
const size_t n_dims_half = n_dims >> 1;
const int n_ctx_orig = rope_params[4];
memcpy(&freq_base, rope_params + 5, sizeof(float));
memcpy(&freq_scale, rope_params + 6, sizeof(float));
memcpy(&ext_factor, rope_params + 7, sizeof(float));
memcpy(&attn_factor, rope_params + 8, sizeof(float));
memcpy(&beta_fast, rope_params + 9, sizeof(float));
memcpy(&beta_slow, rope_params + 10, sizeof(float));
const float theta_scale = powf(freq_base, -2.0f / n_dims);
Output<Node> theta;
float mscale = attn_factor;
if (is_multi) {
const size_t num_pairs = is_vision ? (n_dims > 0 ? (size_t) n_dims : (size_t) (head_dim / 2)) : n_dims_half;
int sections[4];
memcpy(sections, rope_params + 11, sizeof(int) * 4);
int sect_dims = sections[0] + sections[1] + sections[2] + sections[3];
if (sect_dims <= 0) {
sect_dims = num_pairs;
}
int sec_w = sections[0] + sections[1];
int sec_e = sec_w + sections[2];
std::vector<int64_t> gather_indices(num_pairs);
std::vector<float> factor(num_pairs);
for (size_t j = 0; j < num_pairs; j++) {
int sector = j % sect_dims;
int plane = 0;
if (is_imrope) {
if (sector % 3 == 1 && sector < 3 * sections[1]) {
plane = 1;
} else if (sector % 3 == 2 && sector < 3 * sections[2]) {
plane = 2;
} else if (sector % 3 == 0 && sector < 3 * sections[0]) {
plane = 0;
} else {
plane = 3;
}
factor[j] = std::pow(theta_scale, j);
} else if (is_vision) {
int p_idx = 0;
if (sector < sections[0]) {
plane = 0;
p_idx = sector;
} else if (sector < sections[0] + sections[1]) {
plane = 1;
p_idx = sector - sections[0];
} else if (sector < sec_w + sections[2]) {
plane = 2;
p_idx = sector - sec_w;
} else {
plane = 3;
p_idx = sector - sec_e;
}
factor[j] = std::pow(theta_scale, p_idx);
} else {
if (sector >= sections[0] && sector < sec_w) {
plane = 1;
} else if (sector >= sec_w && sector < sec_e) {
plane = 2;
} else if (sector >= sec_e) {
plane = 3;
} else {
plane = 0;
}
factor[j] = std::pow(theta_scale, j);
}
gather_indices[j] = plane;
}
auto gather_indices_const =
std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{num_pairs}, gather_indices);
auto gather_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {4});
inp_pos = std::make_shared<ov::op::v8::Gather>(inp_pos, gather_indices_const, gather_axis);
Output<Node> factor_node =
std::make_shared<ov::op::v0::Constant>(ov::element::f32, ov::Shape{num_pairs}, factor);
if (rope_freqs_weight) {
Output<Node> rope_factors = std::make_shared<ov::op::v8::Slice>(
rope_freqs_weight,
ov::op::v0::Constant::create(ov::element::i64, {1}, {0}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {(int64_t) num_pairs}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {1}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {rope_freqs_weight->get_output_partial_shape(0).rank().get_length() - 1}));
rope_factors = std::make_shared<ov::op::v1::Reshape>(
rope_factors,
ov::op::v0::Constant::create(ov::element::i64, {1}, {(int64_t) num_pairs}),
false);
factor_node = std::make_shared<ov::op::v1::Divide>(factor_node, rope_factors);
}
auto theta_extrap = std::make_shared<ov::op::v1::Multiply>(inp_pos, factor_node);
auto theta_interp = std::make_shared<ov::op::v1::Multiply>(
theta_extrap, ov::op::v0::Constant::create(ov::element::f32, {1}, {freq_scale}));
if (ext_factor == 0.0f) {
theta = theta_interp;
} else {
float corr_dims[2];
ggml_rope_yarn_corr_dims(n_dims, n_ctx_orig, freq_base, beta_fast, beta_slow, corr_dims);
auto ramp_mix = rope_yarn_ramp_mix(num_pairs, corr_dims, ext_factor);
Output<Node> one = ov::op::v0::Constant::create(ov::element::f32, Shape{1, 1, 1, 1}, {1.0f});
auto one_minus_ramp = std::make_shared<ov::op::v1::Subtract>(one, ramp_mix);
theta = std::make_shared<ov::op::v1::Add>(
std::make_shared<ov::op::v1::Multiply>(theta_interp, one_minus_ramp),
std::make_shared<ov::op::v1::Multiply>(theta_extrap, ramp_mix));
mscale *= (1.0f + 0.1f * std::log(1.0f / freq_scale));
}
} else {
std::vector<float> factor(n_dims_half);
Output<Node> freq_factors;
float corr_dims[2];
ggml_rope_yarn_corr_dims(n_dims, n_ctx_orig, freq_base, beta_fast, beta_slow, corr_dims);
factor[0] = 1.0f;
for (size_t i = 1; i < factor.size(); i++) {
factor[i] = theta_scale * factor[i - 1];
}
if (stateful) {
freq_factors =
std::make_shared<ov::op::v0::Constant>(ov::element::f32, ov::Shape{1, 1, factor.size()}, factor);
} else {
freq_factors =
std::make_shared<ov::op::v0::Constant>(ov::element::f32, ov::Shape{1, 1, 1, factor.size()}, factor);
}
if (rope_freqs_weight) {
Output<Node> rope_factors = std::make_shared<ov::op::v8::Slice>(
rope_freqs_weight,
ov::op::v0::Constant::create(ov::element::i64, {1}, {0}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {(int64_t) n_dims_half}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {1}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {rope_freqs_weight->get_output_partial_shape(0).rank().get_length() - 1}));
if (stateful) {
rope_factors = std::make_shared<ov::op::v1::Reshape>(
rope_factors,
ov::op::v0::Constant::create(ov::element::i64, {3}, {(int64_t) 1, (int64_t) 1, (int64_t) n_dims_half}),
false);
} else {
rope_factors = std::make_shared<ov::op::v1::Reshape>(
rope_factors,
ov::op::v0::Constant::create(ov::element::i64, {4}, {(int64_t) 1, (int64_t) 1, (int64_t) 1, (int64_t) n_dims_half}),
false);
}
freq_factors = std::make_shared<ov::op::v1::Divide>(freq_factors, rope_factors);
}
auto theta_extrap = std::make_shared<ov::op::v1::Multiply>(freq_factors, inp_pos);
auto theta_interp = std::make_shared<ov::op::v1::Multiply>(
theta_extrap, ov::op::v0::Constant::create(ov::element::f32, {1}, {freq_scale}));
if (ext_factor == 0.0f) {
theta = theta_interp;
} else {
auto ramp_mix = rope_yarn_ramp_mix(n_dims_half, corr_dims, ext_factor);
Output<Node> one;
if (stateful) {
one = ov::op::v0::Constant::create(ov::element::f32, Shape{1, 1, 1}, {1.0f});
} else {
one = ov::op::v0::Constant::create(ov::element::f32, Shape{1, 1, 1, 1}, {1.0f});
}
auto one_minus_ramp = std::make_shared<ov::op::v1::Subtract>(one, ramp_mix);
theta =
std::make_shared<ov::op::v1::Add>(std::make_shared<ov::op::v1::Multiply>(theta_interp, one_minus_ramp),
std::make_shared<ov::op::v1::Multiply>(theta_extrap, ramp_mix));
mscale *= (1.0f + 0.1f * std::log(1.0f / freq_scale));
}
}
Output<Node> cos_theta = std::make_shared<ov::op::v0::Cos>(theta);
Output<Node> sin_theta = std::make_shared<ov::op::v0::Sin>(theta);
if (mscale != 1.0f) {
auto mscale_node = ov::op::v0::Constant::create(ov::element::f32, Shape{}, {mscale});
cos_theta = std::make_shared<ov::op::v1::Multiply>(cos_theta, mscale_node);
sin_theta = std::make_shared<ov::op::v1::Multiply>(sin_theta, mscale_node);
}
return std::make_pair(sin_theta, cos_theta);
}
ov::Output<ov::Node> process_view_input(const NodeContext & context, int input_index, int slice_len, int axis) {
// Only works for VIEW operations that does a non-strided slice with optinal reshape on the slice result.
// The function only does the slice part, the reshape (if any) should be handled by the caller.
// Default axis is -1, which means slicing the last dimension.
// If the VIEW reshapes the result, `slice_len` should be provided
auto input = context.get_input(input_index);
auto * op_params = (size_t *) context.get_input_op_params(input_index);
auto src_stride = context.get_input_stride(input_index);
int64_t slice_start = op_params[0] / src_stride[3];
if (slice_len == 0) {
slice_len = context.get_input_shape(input_index)[3].get_length();
}
int64_t slice_end = slice_start + slice_len;
auto begin = ov::op::v0::Constant::create(ov::element::i64, {1}, {slice_start});
auto end = ov::op::v0::Constant::create(ov::element::i64, {1}, {slice_end});
auto stride = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
ov::Output<ov::Node> axes;
if (axis == -1) {
axes = ov::op::v0::Constant::create(ov::element::i64, {1}, {context.is_stateful() ? 2 : 3});
} else {
axes = ov::op::v0::Constant::create(ov::element::i64, {1}, {axis});
}
auto sliced = std::make_shared<ov::op::v8::Slice>(input, begin, end, stride, axes);
return sliced;
}
ov::Output<ov::Node> process_view_input_new(const NodeContext & context, int input_index) {
auto input = context.get_input(input_index);
// Check if this input has view inputs
size_t view_input_size = context.get_view_input_size(input_index);
if (view_input_size == 0) {
// No view inputs, return the input as is
return input;
}
// If translate_view already resolved this VIEW (produced a Slice), the input
// will already have the expected shape — skip re-slicing.
//
// Two notions of "matches" are accepted per axis:
// - both dims static and equal, OR
// - both dims dynamic.
// The dynamic case matters for the MoE expert-plane views: translate_view now emits a
// DYNAMIC-token slice (so the token dim is not frozen). An all-static-only check would
// see the dynamic token dim, decide the shapes "don't match", and fall through to
// re-slice/flatten the already-resolved view (a Reshape to the full flattened
// n_expert_used*n_embd tail, which then conflicts with the single-plane input). Treat a
// dynamic-vs-dynamic axis as matching so the already-resolved view is reused as-is.
//
// A third case matters for split-model MoE fragments: translate_view resolves the
// expert-plane view against the fragment's INPUT parameter. When the graph is split
// the token axis of that parameter may already be concrete (static n_tokens) even
// though get_view_input_ov_shape() still reports it as dynamic (-1). The resolved
// view is then static [1,1,n_tokens,n_embd] while `expected` is [1,1,?,n_embd].
// An "expected dynamic, actual static" axis is a valid concretization of the SAME
// resolved view, so treat it as matching too. Falling through to process_single_view
// here would re-slice/re-flatten the already-resolved single-plane view against the
// recorded (multi-plane) source strides and emit a constant-target Reshape whose baked
// dims no longer divide the concretized input -> "dimensions do not evenly divide".
// A fourth case matters for stateful execution: `expected` comes from ggml metadata and is
// always GGML_MAX_DIMS ranks, but stateful drops the leading size-1 batch dim, so the
// resolved view is one rank lower. Compare the common trailing dims and require the
// leading expected dims we skip to be 1. Without this the rank test below never matches on
// the stateful path and every already-resolved MoE expert-plane view is re-sliced.
auto expected_ov_shape = context.get_view_input_ov_shape(input_index, 0);
auto actual_shape = input.get_partial_shape();
if (expected_ov_shape.rank().is_static() && actual_shape.rank().is_static() &&
expected_ov_shape.rank().get_length() >= actual_shape.rank().get_length()) {
const int64_t n_actual = actual_shape.rank().get_length();
const int64_t shift = expected_ov_shape.rank().get_length() - n_actual;
bool shapes_match = true;
for (int64_t i = 0; i < shift; ++i) {
if (!expected_ov_shape[i].is_static() || expected_ov_shape[i].get_length() != 1) {
shapes_match = false;
break;
}
}
for (int64_t i = 0; i < n_actual && shapes_match; ++i) {
const auto & exp = expected_ov_shape[i + shift];
const bool both_dynamic = exp.is_dynamic() && actual_shape[i].is_dynamic();
const bool both_static_equal =
exp.is_static() && actual_shape[i].is_static() && exp == actual_shape[i];
// expected dynamic, actual static: the resolved view already carries the
// concrete size for this fragment; reuse it rather than re-materializing.
const bool expected_dyn_actual_static = exp.is_dynamic() && actual_shape[i].is_static();
if (!both_dynamic && !both_static_equal && !expected_dyn_actual_static) {
shapes_match = false;
break;
}
}
if (shapes_match) {
return input;
}
}
// In static mode, use Split instead of Slice for single-dimension reductions.
// This ensures NPUW's FOLD doesn't parametrize per-layer slice indices (which
// would introduce dynamic shapes). A shared Split node sits outside the repeated
// subgraph boundary; each layer receives one of its output ports.
if (context.is_static() && view_input_size == 1) {
auto view_stride_v = context.get_view_input_stride(input_index, 0);
auto view_src_stride_v = context.get_view_input_src_stride(input_index, 0);
auto view_ggml_shape = context.get_view_input_ggml_shape(input_index, 0);
auto view_src_ggml_shape = context.get_view_input_src_ggml_shape(input_index, 0);
auto view_offset = context.get_view_input_offset(input_index, 0);
auto view_src_offset = context.get_view_input_src_offset(input_index, 0);
size_t ndims = view_ggml_shape.size();
std::vector<int> diff_dims;
if (view_src_ggml_shape.size() == ndims) {
for (size_t i = 0; i < ndims; ++i) {
if (view_ggml_shape[i] != view_src_ggml_shape[i]) {
diff_dims.push_back(static_cast<int>(i));
}
}
}
if (diff_dims.size() == 1) {
int split_dim = diff_dims[0];
int64_t num_splits = static_cast<int64_t>(view_src_ggml_shape[split_dim]);
int64_t chunk_size = static_cast<int64_t>(view_ggml_shape[split_dim]);
// Only apply when slicing exactly 1 element from a multi-element dimension
if (chunk_size == 1 && num_splits > 1) {
// Check suffix strides match (dimensions after split_dim)
bool suffix_ok = view_stride_v.size() == view_src_stride_v.size();
if (suffix_ok) {
for (size_t i = static_cast<size_t>(split_dim) + 1; i < ndims; ++i) {
if (view_stride_v[i] != view_src_stride_v[i]) {
suffix_ok = false;
break;
}
}
}
if (suffix_ok && view_src_stride_v[split_dim] > 0) {
size_t relative_offset = view_offset >= view_src_offset ? view_offset - view_src_offset : 0;
int64_t split_index = static_cast<int64_t>(relative_offset / view_src_stride_v[split_dim]);
if (split_index >= 0 && split_index < num_splits) {
auto src_node = input.get_node_shared_ptr();
std::string rt_key = "split_dim_" + std::to_string(split_dim);
auto & rt_info = src_node->get_rt_info();
if (rt_info.find(rt_key) == rt_info.end()) {
auto axis_const =
ov::op::v0::Constant::create(ov::element::i64, {}, {static_cast<int64_t>(split_dim)});
auto split_node =
std::make_shared<ov::op::v1::Split>(input, axis_const, static_cast<size_t>(num_splits));
split_node->set_friendly_name(src_node->get_friendly_name() + "_split");
rt_info[rt_key] = split_node;
}
auto split_node = rt_info[rt_key].as<std::shared_ptr<ov::op::v1::Split>>();
return split_node->output(static_cast<size_t>(split_index));
}
}
}
}
}
// Lambda function to process a single view operation
auto process_single_view =
[](ov::Output<ov::Node> current, size_t view_offset, const std::vector<size_t> & view_stride,
const ov::Shape & view_ggml_shape, const ov::PartialShape & view_ov_shape, const std::string & view_name,
size_t view_src_offset, const std::vector<size_t> & view_src_stride, const ov::Shape & view_src_ggml_shape,
const ov::PartialShape & view_src_ov_shape, const std::string & view_src_name) -> ov::Output<ov::Node> {
// Stateful execution drops a leading size-1 axis, so `current` can be one rank lower
// than the ggml shape metadata (view_stride/view_ggml_shape, always GGML_MAX_DIMS
// entries) assumes. Shift any axis index built from that metadata down by the
// difference before using it as an OV Slice axis.
const auto current_rank = current.get_partial_shape().rank();
const int axis_shift = current_rank.is_static() ?
static_cast<int>(view_stride.size()) - static_cast<int>(current_rank.get_length()) :
0;
auto build_reshape_pattern = [](const ov::PartialShape & target_ov_shape,
const ov::Shape & target_ggml_shape) -> std::vector<int64_t> {
const size_t ndims = target_ggml_shape.size();
std::vector<int64_t> reshape_pattern(ndims);
size_t dynamic_dims = 0;
if (target_ov_shape.rank().is_static() &&
target_ov_shape.rank().get_length() == static_cast<int64_t>(ndims)) {
for (size_t i = 0; i < ndims; ++i) {
if (target_ov_shape[i].is_static()) {
reshape_pattern[i] = target_ov_shape[i].get_length();
} else {
reshape_pattern[i] = -1;
++dynamic_dims;
}
}
} else {
dynamic_dims = 2;
}
if (dynamic_dims > 1) {
for (size_t i = 0; i < ndims; ++i) {
reshape_pattern[i] = static_cast<int64_t>(target_ggml_shape[i]);
}
}
return reshape_pattern;
};
auto build_prefix_tail_reshape_pattern = [](const ov::PartialShape & target_ov_shape,
const ov::Shape & target_ggml_shape, size_t prefix_dims,
int64_t tail_dim) -> std::vector<int64_t> {
std::vector<int64_t> reshape_pattern(prefix_dims + 1);
size_t dynamic_dims = 0;
if (target_ov_shape.rank().is_static() &&
target_ov_shape.rank().get_length() == static_cast<int64_t>(target_ggml_shape.size())) {
for (size_t i = 0; i < prefix_dims; ++i) {
if (target_ov_shape[i].is_static()) {
reshape_pattern[i] = target_ov_shape[i].get_length();
} else {
reshape_pattern[i] = -1;
++dynamic_dims;
}
}
} else {
dynamic_dims = 2;
}
if (dynamic_dims > 1) {
for (size_t i = 0; i < prefix_dims; ++i) {
reshape_pattern[i] = static_cast<int64_t>(target_ggml_shape[i]);
}
}
reshape_pattern[prefix_dims] = tail_dim;
return reshape_pattern;
};
bool same_stride = view_stride.size() == view_src_stride.size();
if (same_stride) {
for (size_t i = 0; i < view_stride.size(); ++i) {
if (view_stride[i] != view_src_stride[i]) {
same_stride = false;
break;
}
}
}
bool same_ggml_shape = view_ggml_shape.size() == view_src_ggml_shape.size();
if (same_ggml_shape) {
for (size_t i = 0; i < view_ggml_shape.size(); ++i) {
if (view_ggml_shape[i] != view_src_ggml_shape[i]) {
same_ggml_shape = false;
break;
}
}
}
if (same_stride && same_ggml_shape) {
return current;
}
if (same_stride) {
const size_t relative_offset = view_offset >= view_src_offset ? view_offset - view_src_offset : 0;
const size_t ndims = view_stride.size();
std::vector<int> diff_dims;
if (view_ggml_shape.size() == ndims && view_src_ggml_shape.size() == ndims) {
for (size_t i = 0; i < ndims; ++i) {
if (view_ggml_shape[i] != view_src_ggml_shape[i]) {
diff_dims.push_back(static_cast<int>(i));
}
}
}
if (diff_dims.size() == 1) {
const int slice_dim = diff_dims[0];
const int64_t dim_size = static_cast<int64_t>(view_src_ggml_shape[slice_dim]);
if (view_stride[slice_dim] > 0 && relative_offset % view_stride[slice_dim] == 0) {
const int64_t begin_val = static_cast<int64_t>((relative_offset / view_stride[slice_dim]) %
static_cast<size_t>(dim_size));
const int64_t end_val = begin_val + static_cast<int64_t>(view_ggml_shape[slice_dim]);
if (begin_val >= 0 && end_val <= dim_size) {
auto sliced = std::make_shared<ov::op::v8::Slice>(
current, ov::op::v0::Constant::create(ov::element::i64, {1}, {begin_val}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {end_val}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {1}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {slice_dim - axis_shift}));
if (view_ov_shape.is_static()) {
auto reshaped = std::make_shared<ov::op::v1::Reshape>(
sliced,
ov::op::v0::Constant::create(ov::element::i64, {ndims}, view_ov_shape.to_shape()),
false);
reshaped->set_friendly_name(view_name);
return reshaped;
}
sliced->set_friendly_name(view_name);
return sliced;
}
}
int64_t tail_src_elems = 1;
int64_t tail_dst_elems = 1;
for (size_t i = slice_dim; i < ndims; ++i) {
tail_src_elems *= static_cast<int64_t>(view_src_ggml_shape[i]);
tail_dst_elems *= static_cast<int64_t>(view_ggml_shape[i]);
}
const size_t elem_stride = view_stride[ndims - 1];
int64_t tail_begin = 0;
if (elem_stride > 0) {
tail_begin =
static_cast<int64_t>((relative_offset / elem_stride) % static_cast<size_t>(tail_src_elems));
}
const int64_t tail_end = tail_begin + tail_dst_elems;
if (tail_begin >= 0 && tail_end <= tail_src_elems) {
std::vector<int64_t> flat_shape;
flat_shape.reserve(slice_dim);
for (int i = 0; i < slice_dim; ++i) {
flat_shape.push_back(static_cast<int64_t>(view_src_ggml_shape[i]));
}
flat_shape.push_back(tail_src_elems);
const size_t flat_ndims = flat_shape.size();
auto flat = std::make_shared<ov::op::v1::Reshape>(
current, ov::op::v0::Constant::create(ov::element::i64, {flat_ndims}, flat_shape), false);
auto sliced = std::make_shared<ov::op::v8::Slice>(
flat, ov::op::v0::Constant::create(ov::element::i64, {1}, {tail_begin}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {tail_end}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {1}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {slice_dim}));
if (view_ov_shape.is_static()) {
auto reshaped = std::make_shared<ov::op::v1::Reshape>(
sliced, ov::op::v0::Constant::create(ov::element::i64, {ndims}, view_ov_shape.to_shape()),
false);
reshaped->set_friendly_name(view_name);
return reshaped;
}
sliced->set_friendly_name(view_name);
return sliced;
}
}
std::vector<int64_t> begin(ndims, 0);
std::vector<int64_t> end(ndims, 0);
std::vector<int64_t> step(ndims, 1);
std::vector<int64_t> axes(ndims, 0);
size_t remaining_offset = relative_offset;
for (size_t i = 0; i < ndims; ++i) {
axes[i] = static_cast<int64_t>(i);
if (view_stride[i] > 0) {
begin[i] = static_cast<int64_t>(remaining_offset / view_stride[i]);
remaining_offset %= view_stride[i];
}
end[i] = begin[i] + static_cast<int64_t>(view_ggml_shape[i]);
}
bool in_bounds = view_src_ggml_shape.size() == ndims && view_ggml_shape.size() == ndims;
if (in_bounds) {
for (size_t i = 0; i < ndims; ++i) {
if (end[i] > static_cast<int64_t>(view_src_ggml_shape[i])) {
in_bounds = false;
break;
}
}
}
if (in_bounds && remaining_offset == 0) {
auto sliced = std::make_shared<ov::op::v8::Slice>(
current, ov::op::v0::Constant::create(ov::element::i64, {ndims}, begin),
ov::op::v0::Constant::create(ov::element::i64, {ndims}, end),
ov::op::v0::Constant::create(ov::element::i64, {ndims}, step),
ov::op::v0::Constant::create(ov::element::i64, {ndims}, axes));
sliced->set_friendly_name(view_name);
return sliced;
}
} else {
bool same_rank = view_stride.size() == view_src_stride.size() &&
view_ggml_shape.size() == view_src_ggml_shape.size() &&
view_stride.size() == view_ggml_shape.size();
const size_t relative_offset = view_offset >= view_src_offset ? view_offset - view_src_offset : 0;
if (same_rank) {
const size_t ndims = view_ggml_shape.size();
std::vector<int> diff_dims;
for (size_t i = 0; i < ndims; ++i) {
if (view_ggml_shape[i] != view_src_ggml_shape[i]) {
diff_dims.push_back(static_cast<int>(i));
}
}
if (diff_dims.size() == 1) {
const size_t slice_dim = static_cast<size_t>(diff_dims[0]);
bool suffix_stride_match = true;
for (size_t i = slice_dim + 1; i < ndims; ++i) {
if (view_stride[i] != view_src_stride[i]) {
suffix_stride_match = false;
break;
}
}
if (suffix_stride_match && view_src_stride[slice_dim] > 0 &&
relative_offset % view_src_stride[slice_dim] == 0) {
const int64_t begin_val = static_cast<int64_t>(relative_offset / view_src_stride[slice_dim]);
const int64_t end_val = begin_val + static_cast<int64_t>(view_ggml_shape[slice_dim]);
const int64_t dim_size = static_cast<int64_t>(view_src_ggml_shape[slice_dim]);
if (begin_val >= 0 && end_val <= dim_size) {
auto sliced = std::make_shared<ov::op::v8::Slice>(
current, ov::op::v0::Constant::create(ov::element::i64, {1}, {begin_val}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {end_val}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {1}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {static_cast<int64_t>(slice_dim)}));
sliced->set_friendly_name(view_name);
return sliced;
}
}
}
}
size_t view_elems = 1;
size_t src_elems = 1;
if (same_rank) {
for (size_t i = 0; i < view_ggml_shape.size(); ++i) {
view_elems *= view_ggml_shape[i];
src_elems *= view_src_ggml_shape[i];
}
}
bool same_num_elements = same_rank && view_elems == src_elems;
if (same_rank && relative_offset == 0 && same_num_elements) {
auto reshape_pattern = build_reshape_pattern(view_ov_shape, view_ggml_shape);
auto reshaped = std::make_shared<ov::op::v1::Reshape>(
current, ov::op::v0::Constant::create(ov::element::i64, {reshape_pattern.size()}, reshape_pattern),
false);
reshaped->set_friendly_name(view_name);
return reshaped;
}
if (same_rank) {
const size_t ndims = view_ggml_shape.size();
// Match views that can be expressed as a regular strided slice over the
// already reconstructed source tensor, e.g. offset on one axis plus step > 1
// on another axis.
bool is_regular_slice = view_src_ggml_shape.size() == ndims;
std::vector<int64_t> begin(ndims, 0);
std::vector<int64_t> end(ndims, 0);
std::vector<int64_t> step(ndims, 1);
std::vector<int64_t> axes(ndims, 0);
size_t remaining_offset = relative_offset;
if (is_regular_slice) {
for (size_t i = 0; i < ndims; ++i) {
axes[i] = static_cast<int64_t>(i);
if (view_src_stride[i] == 0 || view_stride[i] == 0 ||
view_stride[i] % view_src_stride[i] != 0) {
is_regular_slice = false;
break;
}
step[i] = static_cast<int64_t>(view_stride[i] / view_src_stride[i]);
if (step[i] <= 0) {
is_regular_slice = false;
break;
}
begin[i] = static_cast<int64_t>(remaining_offset / view_src_stride[i]);
remaining_offset %= view_src_stride[i];
if (view_ggml_shape[i] == 0) {
end[i] = begin[i];
continue;
}
end[i] = begin[i] + step[i] * static_cast<int64_t>(view_ggml_shape[i] - 1) + 1;
if (begin[i] < 0 || end[i] > static_cast<int64_t>(view_src_ggml_shape[i])) {
is_regular_slice = false;
break;
}
}
}
if (is_regular_slice && remaining_offset == 0) {
auto sliced = std::make_shared<ov::op::v8::Slice>(
current, ov::op::v0::Constant::create(ov::element::i64, {ndims}, begin),
ov::op::v0::Constant::create(ov::element::i64, {ndims}, end),
ov::op::v0::Constant::create(ov::element::i64, {ndims}, step),
ov::op::v0::Constant::create(ov::element::i64, {ndims}, axes));
sliced->set_friendly_name(view_name);
return sliced;
}
const size_t elem_stride = view_src_stride.back();
const bool aligned_offset = elem_stride > 0 && relative_offset % elem_stride == 0;
if (aligned_offset) {
size_t suffix_start = 0;
size_t expected_stride = elem_stride;
for (int i = static_cast<int>(ndims) - 1; i >= 0; --i) {
if (view_stride[i] != expected_stride) {
suffix_start = static_cast<size_t>(i + 1);
break;
}
expected_stride *= view_ggml_shape[i];
}
size_t prefix_elems = 1;
size_t suffix_elems = 1;
for (size_t i = 0; i < suffix_start; ++i) {
prefix_elems *= view_ggml_shape[i];
}
for (size_t i = suffix_start; i < ndims; ++i) {
suffix_elems *= view_ggml_shape[i];
}
if (prefix_elems > 0 && src_elems % prefix_elems == 0) {
const size_t src_tail_elems = src_elems / prefix_elems;
const int64_t tail_begin = static_cast<int64_t>(relative_offset / elem_stride);
const int64_t tail_end = tail_begin + static_cast<int64_t>(suffix_elems);
if (tail_begin >= 0 && tail_end <= static_cast<int64_t>(src_tail_elems)) {
auto prefix_tail_pattern = build_prefix_tail_reshape_pattern(
view_ov_shape, view_ggml_shape, suffix_start, static_cast<int64_t>(src_tail_elems));
auto prefix_tail = std::make_shared<ov::op::v1::Reshape>(
current,
ov::op::v0::Constant::create(ov::element::i64, {prefix_tail_pattern.size()},
prefix_tail_pattern),
false);
ov::Output<ov::Node> selected = prefix_tail;
if (tail_begin != 0 || tail_end != static_cast<int64_t>(src_tail_elems)) {
selected = std::make_shared<ov::op::v8::Slice>(
prefix_tail, ov::op::v0::Constant::create(ov::element::i64, {1}, {tail_begin}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {tail_end}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {1}),
ov::op::v0::Constant::create(ov::element::i64, {1},
{static_cast<int64_t>(suffix_start)}));
}
auto reshape_pattern = build_reshape_pattern(view_ov_shape, view_ggml_shape);
auto reshaped = std::make_shared<ov::op::v1::Reshape>(
selected,
ov::op::v0::Constant::create(ov::element::i64, {reshape_pattern.size()},
reshape_pattern),
false);
reshaped->set_friendly_name(view_name);
return reshaped;
}
}
}
}
return current;
}
(void) view_name;
(void) view_src_ov_shape;
(void) view_src_name;
return current;
};
// Special case: ggml collapses VIEW-of-VIEW chains so that `view_offs` is always an
// ABSOLUTE offset from the true root allocation, regardless of how many VIEW levels
// are in between (see ggml_new_tensor_impl). `src[0]` is still the immediate op-graph
// parent though, which can be a DIFFERENT (already narrowed) VIEW with the SAME ggml
// shape as this one but a different absolute offset -- e.g. a per-layer deepstack
// slice `view_2d(embd, n_embd, n_tokens, embd->nb[1], layer*n_embd*sizeof(float))`
// whose src[0] ("embd") is itself already a zero-offset VIEW of the true root (the
// padded embedding). Chaining through "embd" here would try to re-slice an already
// 2-narrowed tensor using a root-relative offset, going out of bounds and silently
// falling back to a no-op (returning the wrong, already-resolved sibling slice).
// Detect this (same shape as the immediate src, but different absolute offset) and
// re-slice directly from the untouched root using the innermost view's absolute
// offset against the ROOT's own shape/stride instead of chaining through src[0].
{
auto innermost_offset = context.get_view_input_offset(input_index, 0);
auto innermost_src_offset = context.get_view_input_src_offset(input_index, 0);
auto innermost_shape = context.get_view_input_ggml_shape(input_index, 0);
auto innermost_src_shape = context.get_view_input_src_ggml_shape(input_index, 0);
if (innermost_offset != innermost_src_offset && innermost_shape == innermost_src_shape) {
size_t root_view_idx = view_input_size - 1;
auto root_ggml_shape = context.get_view_input_src_ggml_shape(input_index, root_view_idx);
auto root_stride = context.get_view_input_src_stride(input_index, root_view_idx);
auto root_offset = context.get_view_input_src_offset(input_index, root_view_idx);
auto root_ov_shape = context.get_view_input_src_ov_shape(input_index, root_view_idx);
auto root_name = context.get_view_input_src_name(input_index, root_view_idx);
auto innermost_stride = context.get_view_input_stride(input_index, 0);
auto innermost_ov_shape = context.get_view_input_ov_shape(input_index, 0);
auto innermost_name = context.get_view_input_name(input_index, 0);
return process_single_view(input, innermost_offset, innermost_stride, innermost_shape, innermost_ov_shape,
innermost_name, root_offset, root_stride, root_ggml_shape, root_ov_shape,
root_name);
}
}
// Process views from the base tensor (last) to the current view (first)
// Start with the base tensor
ov::Output<ov::Node> current = input;
// Process each view in reverse order (from base to current)
for (int view_idx = view_input_size - 1; view_idx >= 0; view_idx--) {
auto view_offset = context.get_view_input_offset(input_index, view_idx);
auto view_stride = context.get_view_input_stride(input_index, view_idx);
auto view_ggml_shape = context.get_view_input_ggml_shape(input_index, view_idx);
auto view_ov_shape = context.get_view_input_ov_shape(input_index, view_idx);
auto view_name = context.get_view_input_name(input_index, view_idx);
// print view info
// std::cout << "View " << view_idx << ": name = " << view_name << ", offset = " << view_offset << ", stride = ["
// << view_stride[0] << "," << view_stride[1] << "," << view_stride[2] << "," << view_stride[3]
// << "], ggml shape = [" << view_ggml_shape[0] << "," << view_ggml_shape[1] << ","
// << view_ggml_shape[2] << "," << view_ggml_shape[3] << "], ov shape = " << view_ov_shape << std::endl;
auto view_src_offset = context.get_view_input_src_offset(input_index, view_idx);
auto view_src_stride = context.get_view_input_src_stride(input_index, view_idx);
auto view_src_ggml_shape = context.get_view_input_src_ggml_shape(input_index, view_idx);
auto view_src_ov_shape = context.get_view_input_src_ov_shape(input_index, view_idx);
auto view_src_name = context.get_view_input_src_name(input_index, view_idx);
// print source view info
// std::cout << "View " << view_idx << ": source name = " << view_src_name
// << ", source offset = " << view_src_offset << ", source stride = [" << view_src_stride[0] << ","
// << view_src_stride[1] << "," << view_src_stride[2] << "," << view_src_stride[3]
// << "], source ggml shape = [" << view_src_ggml_shape[0] << "," << view_src_ggml_shape[1] << ","
// << view_src_ggml_shape[2] << "," << view_src_ggml_shape[3]
// << "], source ov shape = " << view_src_ov_shape << std::endl;
current = process_single_view(current, view_offset, view_stride, view_ggml_shape, view_ov_shape, view_name,
view_src_offset, view_src_stride, view_src_ggml_shape, view_src_ov_shape,
view_src_name);
}
return current;
}
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,102 @@
#pragma once
#include "node_context.h"
#include <memory>
#include <openvino/core/node.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <utility>
namespace ov {
namespace frontend {
namespace ggml {
std::string getCurrentTime();
void num_inputs_check(const NodeContext & context, size_t min_inputs, size_t max_inputs);
int non_cont_dim(std::vector<size_t> ne, std::vector<size_t> nb);
template <typename T> std::vector<int> argsort_descend(const std::vector<T> & v) {
std::vector<int> idx(v.size());
std::iota(idx.begin(), idx.end(), 0);
std::sort(idx.begin(), idx.end(), [&v](int i1, int i2) { return v[i1] > v[i2]; });
return idx;
}
template <typename T> std::vector<T> sorted_descend(std::vector<T> v) {
std::sort(v.begin(), v.end(), [](T a, T b) { return a > b; });
return v;
}
template <typename T> bool is_permuted(const std::vector<T> & strides) {
for (size_t i = 0; i < strides.size() - 1; ++i) {
if (strides[i] < strides[i + 1]) {
return true;
}
}
return false;
}
template <typename T> std::vector<T> permute(const std::vector<T> & x, const std::vector<int> & perm) {
std::vector<T> result;
result.reserve(perm.size());
for (int i : perm) {
result.push_back(x[i]);
}
return result;
}
std::shared_ptr<ov::Node> get_dimensions(const std::shared_ptr<ov::op::v3::ShapeOf> & shape,
const std::vector<int> & dims);
std::shared_ptr<ov::Node> get_dimensions(const std::shared_ptr<ov::Node> & node, const std::vector<int> & dims);
OutputVector rename_outputs_with_suffix(const OutputVector & outputs, const std::string & suffix);
std::pair<ov::Output<Node>, ov::Output<Node>> make_sin_cos(int32_t * rope_params,
std::shared_ptr<ov::Node> inp_pos,
std::shared_ptr<ov::Node> rope_freqs_weight = nullptr,
int mode = 0,
bool stateful = false,
int64_t head_dim = 0);
ov::Output<ov::Node> process_view_input(const NodeContext & context, int input_index, int slice_len = 0, int axis = -1);
ov::Output<ov::Node> process_view_input_new(const NodeContext & context, int input_index);
namespace op {
template <typename T> OutputVector translate_1to1_match_2_inputs(const NodeContext & context) {
num_inputs_check(context, 2, 2);
auto input_0 = process_view_input_new(context, 0);
auto input_1 = process_view_input_new(context, 1);
auto output_type = context.get_output_type();
if (input_0.get_element_type() != input_1.get_element_type()) {
if (input_0.get_element_type() != ov::element::f32) {
input_0 = std::make_shared<ov::op::v0::Convert>(input_0, ov::element::f32);
}
if (input_1.get_element_type() != ov::element::f32) {
input_1 = std::make_shared<ov::op::v0::Convert>(input_1, ov::element::f32);
}
}
ov::Output<ov::Node> res = std::make_shared<T>(input_0, input_1);
if (res.get_element_type() != output_type) {
res = std::make_shared<ov::op::v0::Convert>(res, output_type);
}
return rename_outputs_with_suffix({res}, context.get_name());
}
template <typename T> OutputVector translate_1to1_match_1_input(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto res = std::make_shared<T>(input);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
File diff suppressed because it is too large. Load diff
+196
View File
@@ -0,0 +1,196 @@
#include "ggml-decoder.h"
#include "ggml-impl.h"
#include "ggml-openvino-extra.h"
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <openvino/runtime/core.hpp>
#include <openvino/runtime/infer_request.hpp>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
// Cache key for a translated/compiled graph. Node count plus the two end node names identify a
// graph during inference, where the same few graphs repeat for the whole session. The list of
// external input names below tells those apart more precisely, but it walks every node and every
// src slot, so it is built only when GGML_OPENVINO_FULL_GRAPH_KEY is set. Op tests run many small
// graphs that can share a node count and end names, and need the full key.
struct graph_key {
int n_nodes;
int n_leaves;
std::string first_node_name;
std::string last_node_name;
// Each entry: "<node_idx>:<src_idx>:<op_type>"
std::vector<std::string> input_srcs;
graph_key(const ggml_cgraph * cgraph, bool include_inputs = false) : n_nodes(cgraph->n_nodes), n_leaves(cgraph->n_leafs) {
if (n_nodes > 0) {
first_node_name = cgraph->nodes[0]->name;
last_node_name = cgraph->nodes[n_nodes - 1]->name;
}
static const bool full_key = ggml_openvino_getenv_int("GGML_OPENVINO_FULL_GRAPH_KEY") != 0;
if (!full_key && !include_inputs) {
return;
}
std::unordered_set<const ggml_tensor *> node_set;
node_set.reserve(cgraph->n_nodes);
for (int i = 0; i < cgraph->n_nodes; i++) {
node_set.insert(cgraph->nodes[i]);
}
for (int node_idx = 0; node_idx < cgraph->n_nodes; node_idx++) {
const ggml_tensor * node = cgraph->nodes[node_idx];
for (int src_idx = 0; src_idx < GGML_MAX_SRC; src_idx++) {
const ggml_tensor * src = node->src[src_idx];
if (src == nullptr || src->buffer == nullptr || node_set.count(src) || src->buffer->usage == GGML_BACKEND_BUFFER_USAGE_WEIGHTS) {
continue;
}
input_srcs.push_back(std::to_string(node_idx) + ":" + std::to_string(src_idx) + ":" +
GgmlOvDecoder::compute_op_type(node));
}
}
}
bool operator==(const graph_key & other) const {
return n_nodes == other.n_nodes && n_leaves == other.n_leaves &&
first_node_name == other.first_node_name &&
last_node_name == other.last_node_name && input_srcs == other.input_srcs;
}
};
struct graph_key_hash {
size_t operator()(const graph_key & key) const {
size_t hash = std::hash<int>{}(key.n_nodes);
hash ^= std::hash<int>{}(key.n_leaves) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
if (key.n_nodes > 0) {
hash ^= std::hash<std::string>{}(key.first_node_name) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
hash ^= std::hash<std::string>{}(key.last_node_name) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
}
for (const auto & s : key.input_srcs) {
hash ^= std::hash<std::string>{}(s) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
}
return hash;
}
};
struct decoder_runtime_ctx {
decoder_runtime_ctx(std::shared_ptr<std::mutex> mutex) : mutex(std::move(mutex)) {}
std::shared_ptr<std::mutex> mutex;
std::shared_ptr<GgmlOvDecoder> ptr;
};
struct ov_compiled_graph {
ov::CompiledModel decode;
ov::CompiledModel prefill;
std::vector<std::string> input_names;
std::vector<std::string> output_names;
};
// Only compilation and cache publication use this mutex. Requests, decoders and
// sequence state belong to individual backend contexts and never enter this cache.
struct ov_compiled_model_cache {
std::mutex mutex;
std::unordered_map<std::string, ov_compiled_graph> graphs;
size_t backend_count = 0;
};
// Private to one backend instance. Only compiled_cache is shared with other
// instances; clearing these local caches cannot invalidate their requests.
struct ov_runtime_context {
// Serializes calls on this backend only, not inference in other contexts.
std::mutex execution_mutex;
std::shared_ptr<ov_compiled_model_cache> compiled_cache;
mutable std::mutex ctx_mutex;
std::string device;
bool stateful;
std::unordered_map<graph_key, std::shared_ptr<decoder_runtime_ctx>, graph_key_hash> decoder_cache;
std::unordered_map<graph_key, std::shared_ptr<ov::InferRequest>, graph_key_hash> infer_request_cache;
std::unordered_map<graph_key, std::shared_ptr<ov::InferRequest>, graph_key_hash> infer_request_cache_prefill;
std::unordered_map<graph_key, std::vector<std::string>, graph_key_hash> ov_input_names_cache;
std::unordered_map<graph_key, std::vector<std::string>, graph_key_hash> ov_output_names_cache;
size_t stateful_kv_size;
std::map<std::string, std::string> kv_state_input_name_map;
// compute-only copies of graphs that carry unselected ggml_build_forward_select() branches
struct compute_graph {
ggml_cgraph graph;
std::vector<ggml_tensor *> nodes;
};
std::unordered_map<const ggml_cgraph *, compute_graph> compute_graphs;
ov_runtime_context() : device("CPU"), stateful(false), stateful_kv_size(0) {}
void clear_caches_locked() {
decoder_cache.clear();
infer_request_cache.clear();
infer_request_cache_prefill.clear();
ov_input_names_cache.clear();
ov_output_names_cache.clear();
kv_state_input_name_map.clear();
stateful_kv_size = 0;
}
void clear_caches() {
std::lock_guard<std::mutex> lock(ctx_mutex);
clear_caches_locked();
}
};
enum ggml_status ov_graph_compute(struct ggml_cgraph * cgraph, ggml_backend_t backend);
size_t checksum(const void * data, size_t size);
bool save_ggml_tensor_data_to_txt(const ggml_tensor * tensor, const std::string & file_path);
void print_input_tensor_info(const std::string & name, const ov::Tensor & tensor);
void print_output_tensor_info(const std::string & name, const ov::Tensor & tensor, const void * output_dst);
template <typename T>
std::vector<T> pad_input(const T * data,
size_t rows,
size_t cols,
size_t padded_rows,
size_t padded_cols,
T pad_value) {
std::vector<T> padded(padded_rows * padded_cols, pad_value);
for (size_t i = 0; i < std::min(rows, padded_rows); ++i) {
for (size_t j = 0; j < std::min(cols, padded_cols); ++j) {
padded[i * padded_cols + j] = data[i * cols + j];
}
}
return padded;
}
template <typename T>
std::vector<T> pad_input(const ggml_tensor * tensor, size_t padded_rows, size_t padded_cols, T pad_value) {
return pad_input<T>(reinterpret_cast<const T *>(tensor->data),
static_cast<size_t>(tensor->ne[1]), // rows
static_cast<size_t>(tensor->ne[0]), // cols
padded_rows, padded_cols, pad_value);
}
const ggml_tensor * get_inp_pos_tensor(struct ggml_cgraph * cgraph);
int64_t get_inp_pos_n_tokens(struct ggml_cgraph * cgraph, const ggml_tensor * inp_pos);
bool get_is_prefill(struct ggml_cgraph * cgraph, const ggml_tensor * inp_pos);
bool is_naive(struct ggml_cgraph * cgraph);
/**
* @brief Heuristically checks whether the given computation graph is a split-model fragment.
* @param cgraph Pointer to the GGML computation graph to analyze.
* @return true if the graph is identified as split; otherwise false.
*/
bool is_model_splitted(struct ggml_cgraph * cgraph);