char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Zijun YuOpenVINO: Update OV to 2026.3.1, whisper.cpp support, Qwen3.5 on NPU, and new ops (#27843)511f9c137

char/aliceai-t5-moe
19.9 KiB432 linesraw
1#include "translate_session.h"
2
3#include "ggml-impl.h"
4#include "ggml-openvino/ggml-openvino-extra.h"
5#include "ggml-openvino/openvino/node_context.h"
6#include "ggml-openvino/openvino/utils.h"
7#include "input_model.h"
8#include "pass/fuse_to_conv.h"
9#include "pass/mark_decompression_convert_constant_folding.h"
10#include "pass/mark_dequantization_subgraph.h"
11#include "pass/squeeze_matmul.h"
12#include "rt_info/weightless_caching_attributes.hpp"
13
14#include <algorithm>
15#include <cstdint>
16#include <cstdlib>
17#include <map>
18#include <memory>
19#include <openvino/core/node.hpp>
20#include <openvino/core/preprocess/pre_post_process.hpp>
21#include <openvino/core/shape.hpp>
22#include <openvino/core/type/element_type.hpp>
23#include <openvino/op/add.hpp>
24#include <openvino/op/broadcast.hpp>
25#include <openvino/op/concat.hpp>
26#include <openvino/op/convert.hpp>
27#include <openvino/op/convert_like.hpp>
28#include <openvino/op/cos.hpp>
29#include <openvino/op/divide.hpp>
30#include <openvino/op/gather.hpp>
31#include <openvino/op/multiply.hpp>
32#include <openvino/op/parameter.hpp>
33#include <openvino/op/range.hpp>
34#include <openvino/op/reshape.hpp>
35#include <openvino/op/result.hpp>
36#include <openvino/op/sin.hpp>
37#include <openvino/op/slice.hpp>
38#include <openvino/op/squeeze.hpp>
39#include <openvino/op/strided_slice.hpp>
40#include <openvino/op/transpose.hpp>
41#include <openvino/op/unsqueeze.hpp>
42#include <openvino/pass/constant_folding.hpp>
43#include <openvino/pass/make_stateful.hpp>
44#include <sstream>
45
46namespace ov {
47namespace frontend {
48namespace ggml {
49
50using namespace ov::op;
51
52namespace {
53
54std::shared_ptr<ov::op::v0::Parameter> create_parameter(const std::string & name,
55                                                        const ModelInputInfo & input_info) {
56    auto param_node = std::make_shared<ov::op::v0::Parameter>(input_info.type, input_info.shape);
57    param_node->set_friendly_name(name);
58    param_node->output(0).get_tensor().set_names({name});
59    return param_node;
60}
61
62std::shared_ptr<ov::Node> create_extra_input(const std::string & name, const ModelExtraInputInfo & input_info) {
63    if (input_info.is_parameter) {
64        auto param_node = std::make_shared<ov::op::v0::Parameter>(input_info.type, input_info.shape);
65        param_node->set_friendly_name(name);
66        param_node->output(0).get_tensor().set_names({name});
67        return param_node;
68    }
69
70    auto constant = std::make_shared<ov::op::v0::Constant>(input_info.type, input_info.shape,
71                                                          std::vector<int64_t>{input_info.value});
72    constant->set_friendly_name(name);
73    return constant;
74}
75
76ov::pass::MakeStateful::ParamResPairs get_kv_param_res_pairs(
77    const std::shared_ptr<ov::Model> & model,
78    const std::map<std::string, std::string> & kv_param_res_names) {
79    ov::pass::MakeStateful::ParamResPairs pairs;
80    const auto & params = model->get_parameters();
81    const auto & results = model->get_results();
82
83    for (const auto & param_res : kv_param_res_names) {
84        const auto & param_name = param_res.first;
85        const auto & res_name = param_res.second;
86
87        auto param_it = std::find_if(params.begin(), params.end(), [&](const std::shared_ptr<v0::Parameter> & node) {
88            return node->get_friendly_name() == param_name;
89        });
90
91        OPENVINO_ASSERT(param_it != params.end(), "The tensor name ", param_name,
92                        " is not associated with any of "
93                        "Parameters in the network.");
94
95        auto res_it = std::find_if(results.begin(), results.end(), [&](const std::shared_ptr<v0::Result> & node) {
96            return node->get_friendly_name() == res_name;
97        });
98
99        OPENVINO_ASSERT(res_it != results.end(), "The tensor name ", res_name,
100                        " is not associated with any of "
101                        "Results in the network.");
102
103        std::shared_ptr<ov::op::v0::Parameter> param = *param_it;
104        std::shared_ptr<ov::op::v0::Result> res = *res_it;
105        pairs.emplace_back(param, res);
106    }
107    return pairs;
108}
109
110void add_sliced_mask_stateful(TensorMap & tensor_map) {
111    auto create_sliced_mask = [&](const std::string & mask_name, const std::string & sliced_name) {
112        if ((tensor_map.find(mask_name) != tensor_map.end()) &&
113            (tensor_map.find("token_len_per_seq") != tensor_map.end()) &&
114            (tensor_map.find("inp_pos") != tensor_map.end())) {
115            auto token_len_per_seq = tensor_map.at("token_len_per_seq").get_node_shared_ptr();
116            auto mask = tensor_map.at(mask_name).get_node_shared_ptr();
117            std::shared_ptr<ov::Node> mask_sliced = mask;
118            auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
119            auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
120            auto three = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
121            auto neg_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
122
123            auto step = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
124            auto axes = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
125
126            auto inp_pos = tensor_map.at("inp_pos").get_node_shared_ptr();
127            auto last_inp_pos = std::make_shared<ov::op::v8::Gather>(inp_pos, neg_one, three);
128            auto last_inp_pos_1d = std::make_shared<ov::op::v1::Reshape>(
129                last_inp_pos, ov::op::v0::Constant::create(ov::element::i64, {1}, {1}), false);
130            auto last_inp_pos_cvt = std::make_shared<ov::op::v0::Convert>(last_inp_pos_1d, ov::element::i64);
131            auto last_inp_pos_inc = std::make_shared<ov::op::v1::Add>(last_inp_pos_cvt, one);
132
133            mask_sliced = std::make_shared<ov::op::v8::Slice>(mask, zero, last_inp_pos_inc, step, axes);
134            mask_sliced = std::make_shared<ov::op::v0::Convert>(mask_sliced, ov::element::f16);
135            mask_sliced->set_friendly_name(sliced_name);
136
137            tensor_map.insert({sliced_name, mask_sliced->output(0)});
138        }
139    };
140
141    create_sliced_mask("self_kq_mask", "KQ_mask_sliced");
142    create_sliced_mask("KQ_mask", "KQ_mask_sliced");
143    create_sliced_mask("self_kq_mask_swa", "KQ_mask_swa_sliced");
144}
145
146void add_rope_sin_cos(TensorMap & tensor_map, GgmlDecoder & ggml_model_decoder) {
147    // When ROPE ops in the graph have divergent op_params (e.g. gemma4's mixed
148    // SWA/non-SWA layers with different n_dims or freq_base), a shared sin/cos
149    // precompute cannot broadcast across every ROPE use. Skip it here and let
150    // translate_rope() build sin/cos per-op from its own op_params.
151    if (ggml_model_decoder.has_mixed_rope_params()) {
152        return;
153    }
154    int32_t * rope_params = ggml_model_decoder.get_rope_params();
155    if (tensor_map.find("inp_pos") == tensor_map.end() || rope_params == nullptr) {
156        return;
157    }
158    auto inp_pos = tensor_map.at("inp_pos").get_node_shared_ptr();
159    std::shared_ptr<ov::Node> rope_freqs_weight;
160    if (tensor_map.find("rope_freqs.weight") != tensor_map.end()) {
161        rope_freqs_weight = tensor_map.at("rope_freqs.weight").get_node_shared_ptr();
162    }
163
164    auto sin_cos = make_sin_cos(rope_params, inp_pos, rope_freqs_weight);
165    auto sin_theta = sin_cos.first;
166    auto cos_theta = sin_cos.second;
167
168    cos_theta.get_node_shared_ptr()->set_friendly_name("rope_cos");
169    sin_theta.get_node_shared_ptr()->set_friendly_name("rope_sin");
170    tensor_map.insert({"rope_cos", cos_theta});
171    tensor_map.insert({"rope_sin", sin_theta});
172}
173
174// Create common patterns
175void preprocess(TensorMap & tensor_map, GgmlDecoder & ggml_model_decoder) {
176    if (ggml_model_decoder.is_stateful()) {
177        add_sliced_mask_stateful(tensor_map);
178    }
179    // This optimization is error-prone
180    // add_rope_sin_cos(tensor_map, ggml_model_decoder);
181}
182
183}  // namespace
184
185TranslateSession::TranslateSession(const frontend::InputModel::Ptr & input_model,
186                                   const std::unordered_map<std::string, CreatorFunction> & translator_map,
187                                   bool naive) :
188    m_input_model(input_model),
189    m_translator_map(translator_map),
190    m_ov_model(nullptr),
191    m_naive(naive) {}
192
193std::shared_ptr<Model> TranslateSession::get_converted_model() {
194    if (m_ov_model) {
195        return m_ov_model;
196    }
197    m_ov_model = translate_graph(m_input_model);
198    return m_ov_model;
199}
200
201std::shared_ptr<Model> TranslateSession::translate_graph(const frontend::InputModel::Ptr & input_model) {
202    ov::ParameterVector params;
203    ov::ResultVector results;
204    auto tensor_map = std::make_shared<TensorMap>();
205    std::shared_ptr<Model> resulting_model;
206
207    const auto & ggml_model = std::dynamic_pointer_cast<InputModel>(input_model);
208    std::shared_ptr<GgmlDecoder> ggml_model_decoder = ggml_model->get_model_decoder();
209
210    for (const auto & it : ggml_model_decoder->get_model_inputs()) {
211        auto param_node = create_parameter(it.first, it.second);
212        params.push_back(param_node);
213        (*tensor_map)[it.first] = param_node;
214    }
215
216    for (const auto & it : ggml_model_decoder->get_model_extra_inputs()) {
217        auto input_node = create_extra_input(it.first, it.second);
218        if (it.second.is_parameter) {
219            params.push_back(std::dynamic_pointer_cast<ov::op::v0::Parameter>(input_node));
220        }
221        (*tensor_map)[it.first] = input_node;
222    }
223
224    for (const auto & it : ggml_model_decoder->get_model_weights()) {
225        (*tensor_map)[it.first] = it.second;
226    }
227
228    auto translate_node = [&](const std::shared_ptr<GgmlDecoder> & decoder, int node_idx) {
229        auto operation_type = decoder->get_op_type(node_idx);
230        if (operation_type == "GGML_OP_NONE") {
231            return ov::OutputVector{};
232        }
233
234        auto it = m_translator_map.find(operation_type);
235        FRONT_END_OP_CONVERSION_CHECK(it != m_translator_map.end(), "Translation for operation type ", operation_type,
236                                      " is not implemented.");
237        NodeContext node_context(decoder, tensor_map, node_idx, this);
238        ov::OutputVector converted_outputs = it->second(node_context);
239
240        const auto & node_output_names = decoder->get_output_names(node_idx);
241        FRONT_END_OP_CONVERSION_CHECK(node_output_names.size() == converted_outputs.size(), "Number of ",
242                                      operation_type, " outputs greater than number of converted outputs, which are ",
243                                      node_output_names.size(), " and ", converted_outputs.size(), " respectively.");
244
245        for (size_t i = 0; i < node_output_names.size(); ++i) {
246            auto output_name = node_output_names[i];
247            if (i < converted_outputs.size() && converted_outputs[i].get_node_shared_ptr() != nullptr) {
248                (*tensor_map)[output_name] = converted_outputs[i];
249            }
250        }
251        return converted_outputs;
252    };
253
254    // To handle cases like this
255    // 3: [ 18432,     1,     1,     1] RESHAPE              cache_r_l0 (reshaped)#3
256    //     [ 18432,     1,     1,     1]            0: NONE        cache_r_l0
257    // 4: [     0,     1,     1,     1] VIEW                 cache_r_l0 (reshaped) (view)#4
258    //     [ 18432,     1,     1,     1]            0: RESHAPE     cache_r_l0 (reshaped)#3
259    // 5: [     0,     1,     1,     1] SCALE                cache_r_l0 (reshaped) (view) (view)#5
260    //     [     0,     1,     1,     1]            0: VIEW        cache_r_l0 (reshaped) (view)#4
261    // 6: [     1,     1,     1,     1] VIEW                  (view)#6
262    //     [     1,     1,     1,     1]            0: NONE        leaf_5
263    // 7: [ 18432,     1,     1,     1] GET_ROWS             conv_states-0#7
264    //     [ 18432,     1,     1,     1]            0: RESHAPE     cache_r_l0 (reshaped)#3
265    //     [     1,     1,     1,     1]            1: VIEW         (view)#6
266    // The scale is in-place which modifies cache_r_l0 (reshaped)#3
267    // The translation of scale overwrites cache_r in the tensor_map,
268    // but we also need to overwrite the old cache_r_l0 (reshaped)#3
269    auto refresh_inplace_aliases = [&](const std::shared_ptr<GgmlDecoder> & decoder, int inplace_node_idx,
270                                       const std::string & view_src_name) {
271        for (int node_idx = 0; node_idx < inplace_node_idx; node_idx++) {
272            if (decoder->is_view_like_alias_of(node_idx, view_src_name)) {
273                translate_node(decoder, node_idx);
274            }
275        }
276    };
277
278    auto node_visitor = [&](std::shared_ptr<GgmlDecoder> decoder, int node_idx) {
279        auto converted_outputs = translate_node(decoder, node_idx);
280        if (converted_outputs.empty()) {
281            return;
282        }
283        const auto inplace_src = decoder->get_inplace_op_src(node_idx);
284        if (inplace_src.empty()) {
285            return;
286        }
287        if (converted_outputs[0].get_node_shared_ptr() != nullptr) {
288            (*tensor_map)[inplace_src] = converted_outputs[0];
289        }
290        refresh_inplace_aliases(decoder, node_idx, inplace_src);
291    };
292
293    if (!m_naive) {
294        preprocess(*tensor_map, *ggml_model_decoder);
295    }
296    ggml_model_decoder->visit_subgraph(node_visitor);
297
298    for (const auto & name : ggml_model_decoder->get_model_output_names()) {
299        FRONT_END_GENERAL_CHECK(tensor_map->find(name) != tensor_map->end(),
300                                "Output name not found in tensor map: ", name);
301        auto result = std::make_shared<v0::Result>(tensor_map->at(name));
302        result->set_friendly_name(name);
303        results.push_back(result);
304    }
305
306    // Debug-only hook: GGML_OPENVINO_DEBUG_NODE=<name1>,<name2>,... adds extra
307    // Result nodes for arbitrary intermediate tensors (looked up by name in
308    // tensor_map), on top of the real model outputs above. These debug
309    // Results are deliberately NOT added to ggml_decoder's model outputs, so
310    // the caller (ov_graph_compute_dynamic in utils.cpp) will not bind them
311    // to any ggml tensor buffer -- OpenVINO allocates its own tensor for
312    // them. This avoids the risk of reading a ggml buffer that has since
313    // been overwritten by a later in-place op (ggml aggressively reuses
314    // buffers), which can happen if trying to inspect an intermediate value
315    // via GGML_OPENVINO_DEBUG_OUTPUT by hacking it into a real output.
316    //
317    // tensor_map keys are usually the plain ggml tensor name (e.g. "embd"),
318    // but tensors that are recomputed multiple times in the same cgraph
319    // (GGML_TENSOR_FLAG_COMPUTE) are disambiguated with a "#<hash>" suffix
320    // (e.g. "cache_k_l0#4853", see get_tensor_ov_name()) which is not
321    // predictable ahead of time. To keep the env var usable, a requested
322    // name is matched either exactly, or as the "name" part before "#" of a
323    // suffixed key (first match wins; ambiguous requests should include the
324    // full "name#hash" form seen in a previous run's log/dump).
325    if (const char * debug_nodes = ggml_openvino_getenv_str("GGML_OPENVINO_DEBUG_NODE")) {
326        std::stringstream ss(debug_nodes);
327        std::string name;
328        while (std::getline(ss, name, ',')) {
329            auto it = tensor_map->find(name);
330            if (it == tensor_map->end()) {
331                it = std::find_if(tensor_map->begin(), tensor_map->end(), [&](const auto & entry) {
332                    return entry.first.compare(0, name.size(), name) == 0 && entry.first.size() > name.size() &&
333                           entry.first[name.size()] == '#';
334                });
335            }
336            if (it == tensor_map->end()) {
337                GGML_LOG_WARN("GGML_OPENVINO_DEBUG_NODE: node '%s' not found in tensor map, skipping\n", name.c_str());
338                continue;
339            }
340            auto result = std::make_shared<v0::Result>(it->second);
341            result->set_friendly_name("__debug_" + it->first);
342            results.push_back(result);
343        }
344    }
345
346    ov::ParameterVector used_params;
347    for (const auto & param : params) {
348        if (!param->output(0).get_target_inputs().empty()) {
349            used_params.push_back(param);
350        }
351    }
352    // if (auto diff = params.size() - used_params.size()) {
353    //     GGML_LOG_INFO("%zu parameters are not used in the model.", diff);
354    // }
355    resulting_model = std::make_shared<Model>(results, used_params);
356
357    apply_transformations(resulting_model);
358
359    // Set WeightlessCacheAttribute on large constants to avoid unnecessary memory copies
360    // in the NPUW plugin. Without this attribute, NPUW's LazyTensor constructor
361    // (lazy_tensor.cpp, op::Const::Const) will memcpy every constant "in case export
362    // occurs", doubling memory usage per compile_model call.
363    //
364    // The bin_offset field serves as a unique key (not a real file offset) — this is
365    // the same convention the GPU plugin uses for non-IR models (see
366    // Plugin::set_weightless_cache_attributes in intel_gpu/src/plugin/plugin.cpp).
367    // Each constant must have a distinct bin_offset, otherwise GPU's weightless cache
368    // import will map multiple constants to the same data.
369    //
370    // Small constants (< 16 elements) are excluded since they may be introduced by
371    // optimization patterns and the overhead is negligible.
372    //
373    // Note: use shape_size() rather than byte_size()/element_type().size() - GatherMatmul's default
374    // bias is a Constant(element::dynamic, Shape{0}), whose element_type().size() is 0 and would
375    // divide by zero.
376    size_t offset = 0;
377    for (auto & node : resulting_model->get_ordered_ops()) {
378        if (auto cnst = ov::as_type_ptr<ov::op::v0::Constant>(node); cnst && ov::shape_size(cnst->get_shape()) >= 16) {
379            auto & rt_info = cnst->get_rt_info();
380            if (rt_info.find(ov::WeightlessCacheAttribute::get_type_info_static()) == rt_info.end()) {
381                rt_info[ov::WeightlessCacheAttribute::get_type_info_static()] =
382                    ov::WeightlessCacheAttribute(cnst->get_byte_size(), offset++, cnst->get_element_type());
383            }
384        }
385    }
386    return resulting_model;
387}
388
389std::shared_ptr<Model> TranslateSession::apply_transformations(std::shared_ptr<Model> model) {
390    auto ggml_model_decoder = std::dynamic_pointer_cast<InputModel>(m_input_model)->get_model_decoder();
391    {
392        ov::pass::Manager manager;
393        manager.set_per_pass_validation(true);
394        manager.register_pass<ov::pass::MarkCompressedFloatConstants>();
395        // Marks the Convert/Subtract/Multiply nodes of our GatherMatmul dequantization chain
396        // (make_int4_weights/make_int8_weights, for_gather_matmul=true) with disable_constant_folding,
397        // so it survives ConstantFolding regardless of whether the target plugin's own
398        // is_decompression_multiply() recognizes GatherMatmul as a valid consumer.
399        manager.register_pass<ov::pass::MarkDequantization>(
400            std::vector<ov::element::Type>{ov::element::u8, ov::element::i8, ov::element::u4, ov::element::i4});
401        manager.register_pass<pass::FuseToConv>();
402
403        if (ggml_model_decoder->is_stateful()) {
404            const auto kv_param_res_names = ggml_model_decoder->get_kv_param_res_names();
405            const auto kv_param_res_pairs = get_kv_param_res_pairs(model, kv_param_res_names);
406            manager.register_pass<ov::pass::MakeStateful>(kv_param_res_pairs);
407        }
408
409        if (ggml_model_decoder->is_static()) {
410            manager.register_pass<pass::SqueezeMatmul>();
411        }
412        manager.run_passes(model);
413        if (ggml_model_decoder->is_stateful()) {
414            ov::preprocess::PrePostProcessor ppp(model);
415            for (size_t i = 0; i < model->get_output_size(); i++) {
416                auto model_output_shape = model->output(i).get_partial_shape();
417                if (model_output_shape.rank().is_static() && model_output_shape.rank().get_length() == 3) {
418                    ppp.output(i).postprocess().custom([](const ov::Output<ov::Node>& node) {
419                        auto axes = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {0});
420                        return std::make_shared<ov::op::v0::Unsqueeze>(node, axes);
421                    });
422                }
423            }
424            model = ppp.build();
425        }
426    }
427    return model;
428}
429
430}  // namespace ggml
431}  // namespace frontend
432}  // namespace ov