char-fork/llama.cpp

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

Zijun YuOpenVINO: Qwen3.5, memory optimization, and test-recurrent-state-rollback (#26952)aee56b3ab

char/aliceai-t5-moe
16.0 KiB303 linesraw
1#include "../node_context.h"
2#include "../op_table.h"
3#include "../utils.h"
4
5#include <cstdint>
6#include <memory>
7#include <openvino/core/node.hpp>
8#include <openvino/core/node_output.hpp>
9#include <openvino/op/add.hpp>
10#include <openvino/op/broadcast.hpp>
11#include <openvino/op/concat.hpp>
12#include <openvino/op/constant.hpp>
13#include <openvino/op/convert.hpp>
14#include <openvino/op/cos.hpp>
15#include <openvino/op/gather.hpp>
16#include <openvino/op/multiply.hpp>
17#include <openvino/op/reshape.hpp>
18#include <openvino/op/shape_of.hpp>
19#include <openvino/op/sin.hpp>
20#include <openvino/op/slice.hpp>
21#include <openvino/op/split.hpp>
22#include <openvino/op/subtract.hpp>
23#include <openvino/op/transpose.hpp>
24#include <openvino/op/unsqueeze.hpp>
25#include <openvino/op/variadic_split.hpp>
26#include <vector>
27
28namespace ov {
29namespace frontend {
30namespace ggml {
31namespace op {
32
33OutputVector translate_rope(const NodeContext & context) {
34    num_inputs_check(context, 2, 3);
35
36    int op_case = context.get_op_case();
37
38    ov::Output<Node> res;
39
40    auto data_node = context.get_input(0).get_node_shared_ptr();
41    auto output_shape = context.get_output_shape().to_shape();
42    int32_t * op_params = context.get_output_op_params();
43    const int mode = op_case;
44    const int64_t head_dim = static_cast<int64_t>(output_shape[3]);
45    const int64_t configured_n_dims = static_cast<int64_t>(op_params[1]);
46    const int64_t n_dims = configured_n_dims == 0 ? head_dim : configured_n_dims;
47
48    constexpr int TYPE_NORMAL = 0;
49    constexpr int TYPE_NEOX = 1;
50    constexpr int TYPE_IMROPE = 2;
51
52    Output<Node> cos_theta_node;
53    Output<Node> sin_theta_node;
54    if (context.has_input("rope_cos")) {
55        cos_theta_node = context.get_input("rope_cos");
56        sin_theta_node = context.get_input("rope_sin");
57    } else {
58        auto inp_pos = context.get_input(1).get_node_shared_ptr();
59        std::shared_ptr<ov::Node> rope_freqs_weight;
60        if (context.get_input_size() == 3) {
61            rope_freqs_weight = context.get_input(2).get_node_shared_ptr();
62        }
63        auto sin_cos = make_sin_cos(op_params, inp_pos, rope_freqs_weight, mode == TYPE_IMROPE, false);
64        sin_theta_node = sin_cos.first;
65        cos_theta_node = sin_cos.second;
66    }
67
68    if (context.get_view_input_size(0) > 0) {
69        data_node = process_view_input_new(context, 0).get_node_shared_ptr();
70        if (context.is_stateful()) {
71            auto data_shape = ov::op::v0::Constant::create(
72                ov::element::i64, {3}, std::vector<int64_t>{-1, (int64_t) output_shape[2], (int64_t) output_shape[3]});
73            data_node = std::make_shared<ov::op::v1::Reshape>(data_node, data_shape, false);
74        } else {
75            auto data_shape = ov::op::v0::Constant::create(
76                ov::element::i64, {4},
77                std::vector<int64_t>{1, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]});
78            data_node = std::make_shared<ov::op::v1::Reshape>(data_node, data_shape, false);
79        }
80    }
81
82    auto output_type = context.get_output_type();
83    if (data_node->get_element_type() != ov::element::f32) {
84        data_node = std::make_shared<ov::op::v0::Convert>(data_node, ov::element::f32);
85    }
86
87    FRONT_END_OP_CONVERSION_CHECK(n_dims > 0 && n_dims <= head_dim && (n_dims % 2 == 0),
88                                  "ROPE expects even n_dims in [1, head_dim]");
89
90    // TODO(openvino-gpu-rope-fusion): TEMPORARY WORKAROUND - do NOT revert until the
91    // OpenVINO GPU plugin is updated.
92    //
93    // For TYPE_NORMAL rope (both stateful and stateless) we emit the Flux-style
94    // interleaved pattern below so the GPU plugin's RoPEFusionFlux matcher folds it
95    // into ov::op::internal::RoPE. The matcher requires rank-4 inputs, which is why
96    // the original even/odd Slice translation (kept in the `else if (mode ==
97    // TYPE_NORMAL)` branch below for reference) does not get fused.
98    //
99    // Once the GPU plugin's RoPE fusion is extended to also recognize the original
100    // even/odd Slice form, this Flux rewrite should be removed and both modes should
101    // be restored to the captured even/odd translation. Until then, keep both paths:
102    // the active Flux rewrite here and the previous translation preserved below.
103    if (mode == TYPE_NORMAL) {
104        auto axis_last = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
105        auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
106        auto step_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
107
108        // Emit the Flux-style interleaved-RoPE pattern so the GPU plugin's
109        // RoPEFusionFlux matcher folds this subgraph into ov::op::internal::RoPE:
110        //   x_paired   = Reshape(x_rot, [1, S, n_heads, n_dims/2, 2])
111        //   x0, x1     = Split(x_paired, axis=-1, num_splits=2)
112        //   x1_neg     = x1 * -1
113        //   x_rotated  = Reshape(Concat([x1_neg, x0], axis=-1), [1, S, n_heads, n_dims])
114        //   y_rot      = x_rot * t_cos + x_rotated * t_sin
115        //   y          = Concat([y_rot, x_tail], axis=-1) if n_dims < head_dim
116        // Mathematically equivalent to the even/odd Slice form below.
117        //
118        // RoPEFusionFlux requires rank_equals(4) on x, t_cos and t_sin. The cos/sin
119        // tables are already built rank-4 ([1, S, 1, head_size/2]) for both modes. In
120        // stateful mode the data arrives rank-3 ([S, n_heads, head_size]), so lift it
121        // to rank-4 ([1, S, n_heads, head_size]) here. Stateful RoPE already produced
122        // rank-4 output, so downstream attention is unaffected.
123        if (context.is_stateful()) {
124            auto r4_shape = ov::op::v0::Constant::create(
125                ov::element::i64, {4},
126                std::vector<int64_t>{1, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]});
127            data_node = std::make_shared<ov::op::v1::Reshape>(data_node, r4_shape, false);
128        }
129        const int64_t n_heads = static_cast<int64_t>(output_shape[2]);
130        const int64_t half = n_dims / 2;
131        auto rot_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_dims});
132        auto rot_data = std::make_shared<ov::op::v8::Slice>(data_node, zero, rot_end, step_one, axis_last);
133
134        auto neg_one_f = ov::op::v0::Constant::create(data_node->get_element_type(), ov::Shape{}, {-1.0f});
135
136        auto paired_shape = ov::op::v0::Constant::create(
137            ov::element::i64, {5}, std::vector<int64_t>{1, -1, n_heads, half, 2});
138        auto x_paired = std::make_shared<ov::op::v1::Reshape>(rot_data, paired_shape, false);
139
140        auto split_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {-1});
141        auto data_split = std::make_shared<ov::op::v1::Split>(x_paired, split_axis, 2);
142        Output<Node> x0 = data_split->outputs()[0];
143        Output<Node> x1 = data_split->outputs()[1];
144
145        auto x1_neg = std::make_shared<ov::op::v1::Multiply>(x1, neg_one_f);
146        auto x_rotated_paired = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{x1_neg, x0}, -1);
147
148        auto flat_shape =
149            ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, -1, n_heads, n_dims});
150        auto x_rotated =
151            std::make_shared<ov::op::v1::Reshape>(x_rotated_paired, flat_shape, false);
152
153        // Expand cos/sin from [..., n_dims/2] to [..., n_dims] by repeating each
154        // entry twice. Use special_zero on the final Reshape so the seq dim passes
155        // through dynamically. Final rank is 4 to satisfy the matcher's predicate.
156        auto expand_cos_sin = [&](Output<Node> cs) {
157            auto cs_unsq = std::make_shared<ov::op::v0::Unsqueeze>(
158                cs, ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}));
159            auto bcast_target = ov::op::v0::Constant::create(
160                ov::element::i64, {5}, std::vector<int64_t>{1, 1, 1, half, 2});
161            auto bcast = std::make_shared<ov::op::v3::Broadcast>(
162                cs_unsq, bcast_target, ov::op::BroadcastType::BIDIRECTIONAL);
163            auto flat = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{0, 0, 0, n_dims});
164            return std::make_shared<ov::op::v1::Reshape>(bcast, flat, true);
165        };
166        Output<Node> cos_full = expand_cos_sin(cos_theta_node);
167        Output<Node> sin_full = expand_cos_sin(sin_theta_node);
168
169        auto y1 = std::make_shared<ov::op::v1::Multiply>(rot_data, cos_full);
170        auto y2 = std::make_shared<ov::op::v1::Multiply>(x_rotated, sin_full);
171        auto rotated = std::make_shared<ov::op::v1::Add>(y1, y2);
172
173        if (n_dims < head_dim) {
174            auto tail_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_dims});
175            auto tail_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {head_dim});
176            auto tail = std::make_shared<ov::op::v8::Slice>(data_node, tail_start, tail_end, step_one, axis_last);
177            res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{rotated, tail}, -1);
178        } else {
179            res = rotated;
180        }
181    }
182    // PRESERVED PREVIOUS TRANSLATION - Re-enable this branch (and remove the Flux branch above) once
183    // the GPU plugin's RoPE fusion is updated to recognize the even/odd Slice form;
184    // see the TODO(openvino-gpu-rope-fusion) note above. Do not delete.
185    //
186    // Original even/odd Slice form. In stateless mode it ran on rank-4 data
187    // ([1, S, n_heads, head_size]); in stateful mode on rank-3 data
188    // ([S, n_heads, head_size]). Either way it does not match RoPEFusionFlux
189    // (which needs rank-4 x in the interleaved layout), so the RoPE stays as
190    // discrete elementwise ops.
191    //
192    // } else if (mode == TYPE_NORMAL) {
193    //     auto neg_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
194    //     auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
195    //     auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
196    //     auto two = ov::op::v0::Constant::create(ov::element::i64, {1}, {2});
197    //     auto end = ov::op::v0::Constant::create(ov::element::i64, {1}, {output_shape[3]});
198    //     Output<Node> even_slice;
199    //     Output<Node> odd_slice;
200    //     // stateful data is rank 3 (unsqueeze at axis 3), stateless is rank 4 (axis 4)
201    //     int32_t unsqueeze_dim = context.is_stateful() ? 3 : 4;
202    //     even_slice = std::make_shared<ov::op::v8::Slice>(data_node, zero, end, two, neg_one);
203    //     odd_slice = std::make_shared<ov::op::v8::Slice>(data_node, one, end, two, neg_one);
204    //
205    //     Output<Node> first_half =
206    //         std::make_shared<ov::op::v1::Subtract>(std::make_shared<ov::op::v1::Multiply>(even_slice, cos_theta_node),
207    //                                                std::make_shared<ov::op::v1::Multiply>(odd_slice, sin_theta_node));
208    //     Output<Node> second_half =
209    //         std::make_shared<ov::op::v1::Add>(std::make_shared<ov::op::v1::Multiply>(even_slice, sin_theta_node),
210    //                                           std::make_shared<ov::op::v1::Multiply>(odd_slice, cos_theta_node));
211    //
212    //     first_half = std::make_shared<ov::op::v0::Unsqueeze>(first_half,
213    //                                                          ov::op::v0::Constant::create(ov::element::i64, {1}, {unsqueeze_dim}));
214    //     second_half = std::make_shared<ov::op::v0::Unsqueeze>(second_half,
215    //                                                           ov::op::v0::Constant::create(ov::element::i64, {1}, {unsqueeze_dim}));
216    //     auto stack = std::make_shared<ov::op::v0::Concat>(OutputVector{first_half, second_half}, unsqueeze_dim);
217    //
218    //     auto data_shape = ov::op::v0::Constant::create(
219    //         ov::element::i64, {4}, std::vector<int64_t>{1, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]});
220    //     res = std::make_shared<ov::op::v1::Reshape>(stack, data_shape, false);
221    else if (mode == TYPE_NEOX) {
222        // In stateful mode the data arrives rank-3 ([S, n_heads, head_size]) while the
223        // cos/sin tables are rank-4 ([1, S, 1, n_dims/2]). The resulting mixed-rank
224        // broadcast in the Multiply below is miscomputed by the OpenVINO GPU plugin,
225        // corrupting the rotated Q/K. Lift the data to rank-4 ([1, S, n_heads, head_size])
226        // first so the RoPE Multiplies are equal-rank, matching the TYPE_NORMAL branch.
227        // Stateful RoPE already produced rank-4 output, so downstream attention is unaffected.
228        if (context.is_stateful()) {
229            auto r4_shape = ov::op::v0::Constant::create(
230                ov::element::i64, {4},
231                std::vector<int64_t>{1, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]});
232            data_node = std::make_shared<ov::op::v1::Reshape>(data_node, r4_shape, false);
233        }
234        auto axis_last = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {-1});
235        std::vector<int64_t> split_lengths = {n_dims / 2, n_dims / 2};
236        if (n_dims < head_dim) {
237            split_lengths.push_back(head_dim - n_dims);
238        }
239
240        auto data_split = std::make_shared<ov::op::v1::VariadicSplit>(
241            data_node, axis_last,
242            ov::op::v0::Constant::create(ov::element::i64, {split_lengths.size()}, split_lengths));
243        Output<Node> slice_data_node_0 = data_split->outputs()[0];
244        Output<Node> slice_data_node_1 = data_split->outputs()[1];
245
246        auto first_half_node = std::make_shared<ov::op::v1::Subtract>(
247            std::make_shared<ov::op::v1::Multiply>(slice_data_node_0, cos_theta_node),
248            std::make_shared<ov::op::v1::Multiply>(slice_data_node_1, sin_theta_node));
249
250        auto second_half_node = std::make_shared<ov::op::v1::Add>(
251            std::make_shared<ov::op::v1::Multiply>(slice_data_node_0, sin_theta_node),
252            std::make_shared<ov::op::v1::Multiply>(slice_data_node_1, cos_theta_node));
253
254        if (n_dims < head_dim) {
255            Output<Node> tail = data_split->outputs()[2];
256            res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{first_half_node, second_half_node, tail}, -1);
257        } else {
258            res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{first_half_node, second_half_node}, -1);
259        }
260    } else if (mode == TYPE_IMROPE) {
261        auto cos_sin_shape = std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{4},
262                                                                    std::vector<int64_t>{1, -1, 1, (n_dims >> 1)});
263        auto cos_reshaped = std::make_shared<ov::op::v1::Reshape>(cos_theta_node, cos_sin_shape, true);
264        auto sin_reshaped = std::make_shared<ov::op::v1::Reshape>(sin_theta_node, cos_sin_shape, true);
265
266        auto split_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {3});
267        std::vector<int64_t> split_lengths = {n_dims / 2, n_dims / 2};
268        if (n_dims < head_dim) {
269            split_lengths.push_back(head_dim - n_dims);
270        }
271
272        auto split_a = std::make_shared<ov::op::v1::VariadicSplit>(
273            data_node, split_axis,
274            ov::op::v0::Constant::create(ov::element::i64, {split_lengths.size()}, split_lengths));
275        auto x0 = split_a->output(0);
276        auto x1 = split_a->output(1);
277        auto mul_a = std::make_shared<ov::op::v1::Multiply>(x0, cos_reshaped);
278        auto mul_b = std::make_shared<ov::op::v1::Multiply>(x1, sin_reshaped);
279        auto sub = std::make_shared<ov::op::v1::Subtract>(mul_a, mul_b);
280
281        auto mul_c = std::make_shared<ov::op::v1::Multiply>(x0, sin_reshaped);
282        auto mul_d = std::make_shared<ov::op::v1::Multiply>(x1, cos_reshaped);
283        auto add = std::make_shared<ov::op::v1::Add>(mul_c, mul_d);
284
285        if (n_dims < head_dim) {
286            auto tail = split_a->output(2);
287            res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{sub, add, tail}, 3);
288        } else {
289            res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{sub, add}, 3);
290        }
291    }
292
293    if (res.get_element_type() != output_type) {
294        res = std::make_shared<ov::op::v0::Convert>(res, output_type);
295    }
296
297    return rename_outputs_with_suffix({res}, context.get_name());
298}
299
300}  // namespace op
301}  // namespace ggml
302}  // namespace frontend
303}  // namespace ov