include llama.cpp since there were some edits
This commit is contained in:
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
|
||||
+29
@@ -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
@@ -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);
|
||||
Reference in new issue
Block a user