Skip to content

Commit cdd6cf0

Browse files
wine99praasz
andauthored
[GPU] Fix GDN func test to use TEMPLATE plugin as reference (#36081)
### Details: - *Ticket:* [CVS-187512](https://jira.devtools.intel.com/browse/CVS-187512) ### Problem: The GPU functional test `smoke_GatedDeltaNetStatic` was failing on A770 (Linux) because it used the CPU plugin as a reference for output comparison, but the CPU plugin is not registered on that test platform. ### Solution: 1. **Add `evaluate()` method to `GatedDeltaNet` op** — implements the reference GDN algorithm directly in the core op, enabling the TEMPLATE plugin to evaluate it. 2. **Update the GPU functional test** — replace `core.compile_model(model, "CPU")` with `ov::test::utils::infer_on_template()`, which uses the TEMPLATE plugin (interpreter backend) as reference. ### Changes: - `src/core/dev_api/openvino/op/gated_delta_net.hpp` — declare `evaluate()` and `has_evaluate()` overrides - `src/core/src/op/gated_delta_net.cpp` — implement `evaluate()` with the GDN reference algorithm (f32 only) - `src/plugins/intel_gpu/tests/functional/single_layer_tests/gated_delta_net.cpp` — use `infer_on_template` instead of CPU plugin --------- Co-authored-by: Pawel Raasz <pawel.raasz@intel.com>
1 parent 3a968b2 commit cdd6cf0

7 files changed

Lines changed: 566 additions & 40 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (C) 2018-2026 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#pragma once
6+
7+
#include <cmath>
8+
#include <vector>
9+
10+
#include "openvino/core/shape.hpp"
11+
#include "openvino/reference/normalize_l2.hpp"
12+
13+
namespace ov::reference {
14+
15+
template <typename T>
16+
void gated_delta_net(const T* q_data,
17+
const T* k_data,
18+
const T* v_data,
19+
const T* state_data,
20+
const T* gate_data,
21+
const T* beta_data,
22+
T* out_data,
23+
T* out_state,
24+
const Shape& q_shape,
25+
const Shape& v_shape,
26+
bool fuse_qk_l2norm,
27+
T q_l2_norm_eps,
28+
T k_l2_norm_eps) {
29+
const size_t B = q_shape[0];
30+
const size_t S = q_shape[1];
31+
const size_t qk_H = q_shape[2];
32+
const size_t D = q_shape[3];
33+
const size_t v_H = v_shape[2];
34+
const size_t Dv = v_shape[3];
35+
const size_t group_size = v_H / qk_H;
36+
37+
const T attn_scale = static_cast<T>(1) / std::sqrt(static_cast<T>(D));
38+
39+
const size_t qk_stride_batch = S * qk_H * D;
40+
const size_t v_stride_batch = S * v_H * Dv;
41+
const size_t gate_beta_stride_batch = S * v_H;
42+
43+
auto dot_product = [](const T* a, const T* b, size_t n) {
44+
T result = static_cast<T>(0);
45+
for (size_t i = 0; i < n; i++) {
46+
result += a[i] * b[i];
47+
}
48+
return result;
49+
};
50+
51+
const Shape norm_shape{D};
52+
const AxisSet norm_axes{0};
53+
54+
for (size_t b = 0; b < B; b++) {
55+
for (size_t h_v = 0; h_v < v_H; h_v++) {
56+
const size_t h_qk = h_v / group_size;
57+
for (size_t d_v = 0; d_v < Dv; d_v++) {
58+
const size_t state_offset = b * v_H * D * Dv + h_v * D * Dv + d_v;
59+
T* state_ptr = out_state + state_offset;
60+
61+
std::vector<T> local_state(D);
62+
const T* src_state = state_data + state_offset;
63+
for (size_t d = 0; d < D; d++) {
64+
local_state[d] = src_state[d * Dv];
65+
}
66+
67+
for (size_t t = 0; t < S; t++) {
68+
const T* q_ptr = q_data + b * qk_stride_batch + t * qk_H * D + h_qk * D;
69+
const T* k_ptr = k_data + b * qk_stride_batch + t * qk_H * D + h_qk * D;
70+
71+
std::vector<T> q_vec(q_ptr, q_ptr + D);
72+
std::vector<T> k_vec(k_ptr, k_ptr + D);
73+
74+
if (fuse_qk_l2norm) {
75+
normalize_l2(q_vec.data(),
76+
q_vec.data(),
77+
norm_shape,
78+
norm_axes,
79+
static_cast<float>(q_l2_norm_eps),
80+
op::EpsMode::ADD);
81+
normalize_l2(k_vec.data(),
82+
k_vec.data(),
83+
norm_shape,
84+
norm_axes,
85+
static_cast<float>(k_l2_norm_eps),
86+
op::EpsMode::ADD);
87+
}
88+
89+
for (size_t i = 0; i < D; i++)
90+
q_vec[i] *= attn_scale;
91+
92+
T g = std::exp(gate_data[b * gate_beta_stride_batch + t * v_H + h_v]);
93+
T bt = beta_data[b * gate_beta_stride_batch + t * v_H + h_v];
94+
95+
for (size_t d = 0; d < D; d++) {
96+
local_state[d] *= g;
97+
}
98+
99+
T h_k = dot_product(local_state.data(), k_vec.data(), D);
100+
101+
T v_val = v_data[b * v_stride_batch + t * v_H * Dv + h_v * Dv + d_v] - h_k;
102+
103+
T update_scale = v_val * bt;
104+
for (size_t d = 0; d < D; d++) {
105+
local_state[d] += k_vec[d] * update_scale;
106+
}
107+
108+
out_data[b * v_stride_batch + t * v_H * Dv + h_v * Dv + d_v] =
109+
dot_product(local_state.data(), q_vec.data(), D);
110+
}
111+
112+
for (size_t d = 0; d < D; d++) {
113+
state_ptr[d * Dv] = local_state[d];
114+
}
115+
}
116+
}
117+
}
118+
}
119+
120+
} // namespace ov::reference

src/core/shape_inference/include/gated_delta_net_shape_inference.hpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#pragma once
66

7+
#include "dimension_util.hpp"
78
#include "openvino/op/gated_delta_net.hpp"
89
#include "utils.hpp"
910

@@ -28,6 +29,7 @@ std::vector<TRShape> shape_infer(const GatedDeltaNet* op, const std::vector<T>&
2829
const auto& k_head_size = key_ps[3];
2930
const auto& q_head_size = query_ps[3];
3031
const auto& v_head_size = value_ps[3];
32+
auto out_shape = TRShape(value_ps);
3133

3234
NODE_SHAPE_INFER_CHECK(op,
3335
input_shapes,
@@ -45,6 +47,15 @@ std::vector<TRShape> shape_infer(const GatedDeltaNet* op, const std::vector<T>&
4547
" and ",
4648
q_head_size);
4749

50+
NODE_SHAPE_INFER_CHECK(
51+
op,
52+
input_shapes,
53+
q_head_num.is_dynamic() || ov::util::dim::is_divisible(out_shape[2], q_head_num.get_length()),
54+
"The number of value heads must be a multiple of query/key heads (GQA), but got v_H=",
55+
v_head_num,
56+
" and qk_H=",
57+
q_head_num);
58+
4859
const auto& gate_head_num = gate_ps[2];
4960
const auto& beta_head_num = beta_ps[2];
5061

@@ -84,6 +95,6 @@ std::vector<TRShape> shape_infer(const GatedDeltaNet* op, const std::vector<T>&
8495
v_head_size);
8596
// output has the same shape and type as input value, output state has the same shape and type as input
8697
// recurrent_state
87-
return {value_ps, state_ps};
98+
return {out_shape, state_ps};
8899
}
89100
} // namespace ov::op::internal

src/plugins/intel_gpu/tests/functional/single_layer_tests/gated_delta_net.cpp

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@
22
// SPDX-License-Identifier: Apache-2.0
33
//
44

5+
#include "openvino/op/gated_delta_net.hpp"
6+
7+
#include "common_test_utils/common_utils.hpp"
58
#include "common_test_utils/ov_tensor_utils.hpp"
9+
#include "common_test_utils/ov_test_utils.hpp"
610
#include "common_test_utils/test_common.hpp"
7-
#include "common_test_utils/common_utils.hpp"
811
#include "openvino/op/parameter.hpp"
912
#include "openvino/op/result.hpp"
10-
#include "openvino/op/gated_delta_net.hpp"
1113
#include "openvino/runtime/core.hpp"
1214

1315
namespace {
1416

15-
using GatedDeltaNetParams = std::tuple<
16-
std::vector<ov::Shape>, // Input shapes: query, key, value, state, gate, beta
17-
ov::element::Type, // Input precision
18-
bool>; // fuse_qk_l2norm
17+
using GatedDeltaNetParams = std::tuple<std::vector<ov::Shape>, // Input shapes: query, key, value, state, gate, beta
18+
ov::element::Type, // Input precision
19+
bool>; // fuse_qk_l2norm
1920

20-
class GatedDeltaNetStaticTest : public testing::WithParamInterface<GatedDeltaNetParams>,
21-
public ov::test::TestsCommon {
21+
class GatedDeltaNetStaticTest : public testing::WithParamInterface<GatedDeltaNetParams>, public ov::test::TestsCommon {
2222
public:
2323
static std::string getTestCaseName(const testing::TestParamInfo<GatedDeltaNetParams>& obj) {
2424
const auto& [input_shapes, precision, fuse_qk_l2norm] = obj.param;
@@ -46,16 +46,12 @@ class GatedDeltaNetStaticTest : public testing::WithParamInterface<GatedDeltaNet
4646
auto gate = std::make_shared<ov::op::v0::Parameter>(precision, input_shapes[4]);
4747
auto beta = std::make_shared<ov::op::v0::Parameter>(precision, input_shapes[5]);
4848

49-
auto gdn = std::make_shared<ov::op::internal::GatedDeltaNet>(
50-
query, key, value, state, gate, beta, fuse_qk_l2norm);
49+
auto gdn = std::make_shared<ov::op::internal::GatedDeltaNet>(query, key, value, state, gate, beta, fuse_qk_l2norm);
5150

5251
auto result0 = std::make_shared<ov::op::v0::Result>(gdn->output(0));
5352
auto result1 = std::make_shared<ov::op::v0::Result>(gdn->output(1));
5453

55-
model = std::make_shared<ov::Model>(
56-
ov::ResultVector{result0, result1},
57-
ov::ParameterVector{query, key, value, state, gate, beta},
58-
"GatedDeltaNetTest");
54+
model = std::make_shared<ov::Model>(ov::ResultVector{result0, result1}, ov::ParameterVector{query, key, value, state, gate, beta}, "GatedDeltaNetTest");
5955
}
6056

6157
std::map<std::shared_ptr<ov::op::v0::Parameter>, ov::Tensor> generate_inputs() {
@@ -66,29 +62,28 @@ class GatedDeltaNetStaticTest : public testing::WithParamInterface<GatedDeltaNet
6662
if (i == 4) {
6763
in_data = ov::test::utils::InputGenerateData(-1, 1, 1000, 1);
6864
}
69-
inputs[params[i]] = ov::test::utils::create_and_fill_tensor(
70-
params[i]->get_element_type(), params[i]->get_shape(), in_data);
65+
inputs[params[i]] = ov::test::utils::create_and_fill_tensor(params[i]->get_element_type(), params[i]->get_shape(), in_data);
7166
}
7267
return inputs;
7368
}
7469

7570
std::shared_ptr<ov::Model> model;
7671
};
7772

78-
TEST_P(GatedDeltaNetStaticTest, CompareWithCPU) {
73+
TEST_P(GatedDeltaNetStaticTest, CompareWithTemplate) {
7974
auto inputs = generate_inputs();
8075

81-
ov::Core core;
82-
83-
// Run on CPU (reference)
84-
auto compiled_cpu = core.compile_model(model, "CPU");
85-
auto req_cpu = compiled_cpu.create_infer_request();
86-
for (const auto& [param, tensor] : inputs) {
87-
req_cpu.set_tensor(param->output(0), tensor);
76+
// Build input tensor vector for infer_on_template
77+
ov::TensorVector input_tensors;
78+
for (const auto& param : model->get_parameters()) {
79+
input_tensors.push_back(inputs.at(param));
8880
}
89-
req_cpu.infer();
81+
82+
// Run on TEMPLATE (reference)
83+
auto ref_outputs = ov::test::utils::infer_on_template(model, input_tensors);
9084

9185
// Run on GPU
86+
ov::Core core;
9287
auto compiled_gpu = core.compile_model(model, "GPU");
9388
auto req_gpu = compiled_gpu.create_infer_request();
9489
for (const auto& [param, tensor] : inputs) {
@@ -98,30 +93,28 @@ TEST_P(GatedDeltaNetStaticTest, CompareWithCPU) {
9893

9994
// Compare outputs
10095
for (size_t i = 0; i < model->get_output_size(); i++) {
101-
auto out_cpu = req_cpu.get_output_tensor(i);
10296
auto out_gpu = req_gpu.get_output_tensor(i);
103-
ov::test::utils::compare(out_cpu, out_gpu, 1e-2, 1e-2);
97+
ov::test::utils::compare(ref_outputs[i], out_gpu, 1e-2, 1e-2);
10498
}
10599
}
106100

107-
// Shapes: query[B,S,H,D], key[B,S,H,D], value[B,S,H,Dv], state[B,H,D,Dv], gate[B,S,H], beta[B,S,H]
101+
// Shapes: query[B,S,qk_H,D], key[B,S,qk_H,D], value[B,S,v_H,Dv], state[B,v_H,D,Dv], gate[B,S,v_H], beta[B,S,v_H]
108102
const std::vector<std::vector<ov::Shape>> static_shapes = {
109-
// B=1, S=1, H=4, D=16, Dv=16 (minimal)
103+
// B=1, S=1, qk_H=4, v_H=4, D=16, Dv=16 (minimal)
110104
{{1, 1, 4, 16}, {1, 1, 4, 16}, {1, 1, 4, 16}, {1, 4, 16, 16}, {1, 1, 4}, {1, 1, 4}},
111-
// B=1, S=1, H=32, D=128, Dv=128 (typical LLM decode)
105+
// B=1, S=1, qk_H=32, v_H=32, D=128, Dv=128 (typical LLM decode)
112106
{{1, 1, 32, 128}, {1, 1, 32, 128}, {1, 1, 32, 128}, {1, 32, 128, 128}, {1, 1, 32}, {1, 1, 32}},
113-
// B=1, S=16, H=2, D=16, Dv=32 (seq_len > 1, different D and Dv)
107+
// B=1, S=16, qk_H=2, v_H=2, D=16, Dv=32 (seq_len > 1, different D and Dv)
114108
{{1, 16, 2, 16}, {1, 16, 2, 16}, {1, 16, 2, 32}, {1, 2, 16, 32}, {1, 16, 2}, {1, 16, 2}},
115-
// B=2, S=1, H=8, D=64, Dv=64 (batch > 1)
109+
// B=2, S=1, qk_H=8, v_H=8, D=64, Dv=64 (batch > 1)
116110
{{2, 1, 8, 64}, {2, 1, 8, 64}, {2, 1, 8, 64}, {2, 8, 64, 64}, {2, 1, 8}, {2, 1, 8}},
111+
// B=1, S=4, qk_H=2, v_H=8, D=16, Dv=16 (GQA: v_H is multiple of qk_H)
112+
{{1, 4, 2, 16}, {1, 4, 2, 16}, {1, 4, 8, 16}, {1, 8, 16, 16}, {1, 4, 8}, {1, 4, 8}},
117113
};
118114

119-
INSTANTIATE_TEST_SUITE_P(
120-
smoke_GatedDeltaNetStatic,
121-
GatedDeltaNetStaticTest,
122-
::testing::Combine(::testing::ValuesIn(static_shapes),
123-
::testing::Values(ov::element::f32),
124-
::testing::Values(false, true)),
125-
GatedDeltaNetStaticTest::getTestCaseName);
115+
INSTANTIATE_TEST_SUITE_P(smoke_GatedDeltaNetStatic,
116+
GatedDeltaNetStaticTest,
117+
::testing::Combine(::testing::ValuesIn(static_shapes), ::testing::Values(ov::element::f32), ::testing::Values(false, true)),
118+
GatedDeltaNetStaticTest::getTestCaseName);
126119

127120
} // namespace
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (C) 2018-2026 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#include "openvino/reference/gated_delta_net.hpp"
6+
7+
#include "evaluate_node.hpp"
8+
#include "gated_delta_net_shape_inference.hpp"
9+
#include "openvino/core/type/element_type_traits.hpp"
10+
#include "openvino/op/gated_delta_net.hpp"
11+
12+
template <ov::element::Type_t ET>
13+
bool evaluate(const std::shared_ptr<ov::op::internal::GatedDeltaNet>& op,
14+
ov::TensorVector& outputs,
15+
const ov::TensorVector& inputs) {
16+
using T = typename ov::element_type_traits<ET>::value_type;
17+
18+
const auto input_shapes = ov::util::get_tensors_partial_shapes(inputs);
19+
const auto output_shapes = ov::op::internal::shape_infer(op.get(), input_shapes);
20+
outputs[0].set_shape(output_shapes[0].to_shape());
21+
outputs[1].set_shape(output_shapes[1].to_shape());
22+
23+
ov::reference::gated_delta_net<T>(inputs[0].data<const T>(),
24+
inputs[1].data<const T>(),
25+
inputs[2].data<const T>(),
26+
inputs[3].data<const T>(),
27+
inputs[4].data<const T>(),
28+
inputs[5].data<const T>(),
29+
outputs[0].data<T>(),
30+
outputs[1].data<T>(),
31+
inputs[0].get_shape(),
32+
inputs[2].get_shape(),
33+
op->get_fuse_qk_l2norm(),
34+
static_cast<T>(op->get_q_l2_norm_eps()),
35+
static_cast<T>(op->get_k_l2_norm_eps()));
36+
return true;
37+
}
38+
39+
template <>
40+
bool evaluate_node<ov::op::internal::GatedDeltaNet>(std::shared_ptr<ov::Node> node,
41+
ov::TensorVector& outputs,
42+
const ov::TensorVector& inputs) {
43+
const auto& element_type = node->get_input_element_type(0);
44+
45+
switch (element_type) {
46+
case ov::element::f32:
47+
return evaluate<ov::element::f32>(ov::as_type_ptr<ov::op::internal::GatedDeltaNet>(node), outputs, inputs);
48+
default:
49+
OPENVINO_THROW("Unhandled data type ", element_type, " in evaluate_node()");
50+
}
51+
}

src/plugins/template/backend/ops/ops_evaluates.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#pragma once
66
#include "evaluate_node.hpp"
7+
#include "openvino/op/gated_delta_net.hpp"
78
#include "openvino/op/ops.hpp"
89
#include "openvino/op/paged_attention.hpp"
910
#include "openvino/op/rms_norm.hpp"
@@ -549,6 +550,10 @@ extern template bool evaluate_node<ov::op::internal::AUGRUSequence>(std::shared_
549550
ov::TensorVector& outputs,
550551
const ov::TensorVector& inputs);
551552

553+
extern template bool evaluate_node<ov::op::internal::GatedDeltaNet>(std::shared_ptr<ov::Node> node,
554+
ov::TensorVector& outputs,
555+
const ov::TensorVector& inputs);
556+
552557
extern template bool evaluate_node<ov::op::internal::RMS>(std::shared_ptr<ov::Node> node,
553558
ov::TensorVector& outputs,
554559
const ov::TensorVector& inputs);

src/plugins/template/backend/opset_int_tbl.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ _OPENVINO_OP_REG(OneHot, ov::op::v16)
188188

189189
_OPENVINO_OP_REG(AUGRUCell, ov::op::internal)
190190
_OPENVINO_OP_REG(AUGRUSequence, ov::op::internal)
191+
_OPENVINO_OP_REG(GatedDeltaNet, ov::op::internal)
191192
_OPENVINO_OP_REG(RMS, ov::op::internal)
192193
_OPENVINO_OP_REG(RMSNorm, ov::op::internal)
193194
_OPENVINO_OP_REG(PagedAttentionExtension, ov::op)

0 commit comments

Comments
 (0)