Skip to content

Commit ea600b7

Browse files
authored
[GPU] Enable int8/int4 quantized-KV GroupQueryAttention (openvinotoolkit#36798)
### Details: Enables the com.microsoft GroupQueryAttention op with an i8/u8 quantized KV cache on the Intel GPU plugin, for both the dynamic (Slice+Concat) and static (ScatterUpdate) cache-assembly paths. This already worked on CPU/TEMPLATE via the device-agnostic GroupQueryAttentionDecomposition; the GPU failures were a few plugin-local primitive gaps where a kernel supported float but not i8/u8. Fixes (all GPU-plugin-local; core op and decomposition untouched): - **KeepGQAKVScalePrecision**: new GPU MatcherPass that marks the KV dequant scales (k_scale/v_scale) precision-sensitive before ConvertPrecision. GPU runs ConvertPrecision (fp32->fp16) before the GQA op is decomposed in CommonOptimizations, and the intact op requires fp32 scales (com.microsoft spec). Mirrors the existing KeepXAttentionThresholdPrecision. Marks only the scale constants (no downstream fp32 propagation), so no extra memory or compute. - **ScatterUpdate (static path)**: adds i8/u8 to the OCL impl's supported input types and to the ref kernel's GetSupportedKey inputs (outputs already had them). Pure index-driven copy, so i8/u8 is numerically safe. - **Slice (dynamic i8 path)**: adds i8 to the ref kernel's supported output types (i8/u8 input and u8 output were already present). Tests: un-skips the 4 IE_GPU gqa i8/i4 conformance tests and adds static-shape (ScatterUpdate, reshape-to-static) variants; present KV is checked bit-exact. Run: `ov_onnx_frontend_tests --gtest_filter='*onnx_model_gqa_i8kv*:*onnx_model_gqa_i4kv*'` → 27/27 (IE_CPU 9, IE_GPU 9, INTERPRETER 9), no CPU/TEMPLATE regression. Out of scope: f8e4m3 KV on GPU (needs an f8e4m3 Slice/reorder layout) — its 2 IE_GPU tests remain skipped as a documented follow-up. ### Tickets: - [CVS-190527](https://jira.devtools.intel.com/browse/CVS-190527) ### AI Assistance: - *AI assistance used: no / yes* - *If yes, summarize how AI was used and what human validation was performed (build/tests/manual checks).*
1 parent a9a431d commit ea600b7

11 files changed

Lines changed: 553 additions & 17 deletions

File tree

src/frontends/onnx/tests/onnx_import_com_microsoft.in.cpp

Lines changed: 272 additions & 5 deletions
Large diffs are not rendered by default.

src/frontends/onnx/tests/runtime/ie/unit_test.manifest

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -359,13 +359,10 @@ IE_GPU.onnx_model_gather_elements_int32_axis_0
359359
IE_GPU.onnx_model_gather_elements_int8_axis_1
360360
IE_GPU.onnx_model_gather_elements_float_3D_axis_2
361361

362-
# GroupQueryAttention with a quantized KV cache appends tokens via ScatterUpdate on i8/u8/f8e4m3
363-
# data, for which the GPU plugin has no layout ("No layout format available for scatterupdate
364-
# ... data_type: i8"). The numerics are covered on CPU/INTERPRETER; GPU is skipped here.
365-
IE_GPU.onnx_model_gqa_i8kv_per_channel
366-
IE_GPU.onnx_model_gqa_i8kv_per_tensor
367-
IE_GPU.onnx_model_gqa_i4kv_per_channel
368-
IE_GPU.onnx_model_gqa_i4kv_per_tensor
362+
# GroupQueryAttention with an f8e4m3 quantized KV cache: the GPU plugin has no f8e4m3 Slice/reorder
363+
# layout yet ("No layout format available"), so the fp8 variants remain skipped on GPU. The i8/i4
364+
# variants are enabled (see KeepGQAKVScalePrecision + i8/u8 ScatterUpdate/Slice support). fp8 GPU is
365+
# tracked as a separate follow-up; numerics stay covered on CPU/INTERPRETER meanwhile.
369366
IE_GPU.onnx_model_gqa_f8e4m3fnkv_per_channel
370367
IE_GPU.onnx_model_gqa_f8e4m3fnkv_per_tensor
371368

src/plugins/intel_gpu/src/graph/impls/ocl/scatter_update.hpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ struct ScatterUpdateImplementationManager : public ImplementationManager {
3737
format::bfwzyx
3838
};
3939

40-
static const std::vector<ov::element::Type_t> supported_in_types = {
41-
ov::element::f32,
42-
ov::element::f16,
43-
ov::element::i32
44-
};
40+
static const std::vector<ov::element::Type_t> supported_in_types = {ov::element::f32,
41+
ov::element::f16,
42+
ov::element::i32,
43+
ov::element::i8,
44+
ov::element::u8};
4545

4646
static const std::vector<ov::element::Type_t> supported_out_types = {
4747
ov::element::f32,

src/plugins/intel_gpu/src/kernel_selector/kernels/scatter_update/scatter_update_kernel_ref.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ ParamsKey ScatterUpdateKernelRef::GetSupportedKey() const {
3737
k.EnableInputDataType(Datatype::F16);
3838
k.EnableInputDataType(Datatype::F32);
3939
k.EnableInputDataType(Datatype::INT32);
40+
k.EnableInputDataType(Datatype::INT8);
41+
k.EnableInputDataType(Datatype::UINT8);
4042
k.EnableOutputDataType(Datatype::F16);
4143
k.EnableOutputDataType(Datatype::F32);
4244
k.EnableOutputDataType(Datatype::INT32);

src/plugins/intel_gpu/src/kernel_selector/kernels/slice/slice_kernel_ref.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ ParamsKey SliceKernelRef::GetSupportedKey() const {
9090
k.EnableOutputDataType(Datatype::F32);
9191
k.EnableOutputDataType(Datatype::INT32);
9292
k.EnableOutputDataType(Datatype::INT64);
93+
k.EnableOutputDataType(Datatype::INT8);
9394
k.EnableOutputDataType(Datatype::UINT8);
9495
k.EnableInputLayout(DataLayout::bfyx);
9596
k.EnableInputLayout(DataLayout::bfzyx);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (C) 2018-2026 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#include "keep_gqa_kv_scale_precision.hpp"
6+
7+
#include "openvino/op/group_query_attention.hpp"
8+
#include "openvino/op/util/precision_sensitive_attribute.hpp"
9+
#include "openvino/pass/pattern/op/wrap_type.hpp"
10+
11+
namespace ov::intel_gpu {
12+
13+
KeepGQAKVScalePrecision::KeepGQAKVScalePrecision() {
14+
auto gqa_m = ov::pass::pattern::wrap_type<ov::op::internal::GroupQueryAttention>();
15+
16+
ov::matcher_pass_callback callback = [=](ov::pass::pattern::Matcher& m) {
17+
auto gqa = ov::as_type_ptr<ov::op::internal::GroupQueryAttention>(m.get_match_root());
18+
if (!gqa)
19+
return false;
20+
21+
if (transformation_callback(gqa))
22+
return false;
23+
24+
// Only a quantized KV cache has scales at inputs 12 (k_scale) / 13 (v_scale).
25+
if (!gqa->is_kv_quantized())
26+
return false;
27+
28+
constexpr size_t k_scale_idx = 12;
29+
constexpr size_t v_scale_idx = 13;
30+
ov::mark_as_precision_sensitive(gqa->input(k_scale_idx));
31+
ov::mark_as_precision_sensitive(gqa->input(v_scale_idx));
32+
return true;
33+
};
34+
35+
auto matcher = std::make_shared<ov::pass::pattern::Matcher>(gqa_m, "KeepGQAKVScalePrecision");
36+
register_matcher(matcher, callback);
37+
}
38+
39+
} // namespace ov::intel_gpu
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (C) 2018-2026 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#pragma once
6+
7+
#include "openvino/pass/matcher_pass.hpp"
8+
9+
namespace ov::intel_gpu {
10+
11+
// Marks GroupQueryAttention KV dequant scales (k_scale/v_scale) precision-sensitive so
12+
// ConvertPrecision keeps them fp32. GPU runs ConvertPrecision before the GQA op is decomposed,
13+
// and the intact op requires fp32 scales (com.microsoft spec). Applies to a quantized KV cache only.
14+
class KeepGQAKVScalePrecision : public ov::pass::MatcherPass {
15+
public:
16+
OPENVINO_MATCHER_PASS_RTTI("ov::intel_gpu::KeepGQAKVScalePrecision");
17+
KeepGQAKVScalePrecision();
18+
};
19+
20+
} // namespace ov::intel_gpu

src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
#include "plugin/transformations/fuse_moe_router_scale.hpp"
101101
#include "plugin/transformations/increase_position_ids_precision.hpp"
102102
#include "plugin/transformations/indirect_kv_cache.hpp"
103+
#include "plugin/transformations/keep_gqa_kv_scale_precision.hpp"
103104
#include "plugin/transformations/keep_moe_3gemm_const_precision.hpp"
104105
#include "plugin/transformations/keep_xattention_threshold_precision.hpp"
105106
#include "plugin/transformations/kv_cache_compression.hpp"
@@ -764,6 +765,9 @@ void TransformationsPipeline::apply(std::shared_ptr<ov::Model> func) {
764765
ov::element::TypeVector{ov::element::i32, ov::element::u32, ov::element::u16}, add_precision_sensitive_convert);
765766
// Keep xattention threshold in fp32 to avoid boundary issues caused by fp16 quantization.
766767
manager.register_pass<ov::intel_gpu::KeepXAttentionThresholdPrecision>();
768+
// Keep GroupQueryAttention quantized-KV scales fp32 through the ConvertPrecision below
769+
// (the intact op requires fp32 scales; it is decomposed later in CommonOptimizations).
770+
manager.register_pass<ov::intel_gpu::KeepGQAKVScalePrecision>();
767771

768772
manager.register_pass<ov::pass::ConvertPrecision>(fp_convert_precision_map,
769773
empty_fuse_map,

src/plugins/intel_gpu/tests/unit/test_cases/scatter_update_gpu_test.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2287,3 +2287,59 @@ TEST(scatter_update_gpu_fp32, d8111_axisB_first_iteration_kernel_check) {
22872287
}
22882288
}
22892289
}
2290+
2291+
// Integer-typed ScatterUpdate (i8/u8). Exercises the byte-movement path enabled for a quantized KV
2292+
// cache (GroupQueryAttention int8/int4). Dictionary is scattered by index; result is exact.
2293+
template <typename T>
2294+
void test_int_d8111_axisB(bool is_caching_test) {
2295+
// Dictionary : 8x1x1x1, Indexes : 4x1x1x1, Updates : 4x1x1x1, Axis : 0
2296+
// Indexes: 4, 3, 1, 7
2297+
// Updates: 9, 10, 11, 12
2298+
// Dictionary: 1, 2, 3, 4, 5, 6, 7, 8
2299+
// Output: 1, 11, 3, 10, 9, 6, 7, 12
2300+
const auto dt = ov::element::from<T>();
2301+
auto& engine = get_test_engine();
2302+
2303+
for (const auto target_format : formats2D) {
2304+
auto input1 = engine.allocate_memory({dt, plain_2d_format, tensor{8, 1, 1, 1}}); // Dictionary
2305+
auto input2 = engine.allocate_memory({data_types::f32, plain_2d_format, tensor{4, 1, 1, 1}}); // Indexes
2306+
auto input3 = engine.allocate_memory({dt, plain_2d_format, tensor{4, 1, 1, 1}}); // Updates
2307+
auto axis = 0;
2308+
2309+
set_values<T>(input1, {T(1), T(2), T(3), T(4), T(5), T(6), T(7), T(8)});
2310+
set_values(input2, {4.f, 3.f, 1.f, 7.f});
2311+
set_values<T>(input3, {T(9), T(10), T(11), T(12)});
2312+
2313+
topology topology;
2314+
topology.add(input_layout("InputDictionary", input1->get_layout()));
2315+
topology.add(input_layout("InputText", input2->get_layout()));
2316+
topology.add(input_layout("InputUpdates", input3->get_layout()));
2317+
topology.add(reorder("DictionaryReordered", input_info("InputDictionary"), target_format, dt));
2318+
topology.add(reorder("TextReordered", input_info("InputText"), target_format, data_types::f32));
2319+
topology.add(reorder("UpdatesReordered", input_info("InputUpdates"), target_format, dt));
2320+
topology.add(scatter_update("scatter_update", input_info("DictionaryReordered"), input_info("TextReordered"), input_info("UpdatesReordered"), axis));
2321+
topology.add(reorder("out", input_info("scatter_update"), plain_2d_format, dt));
2322+
2323+
cldnn::network::ptr network = get_network(engine, topology, get_test_default_config(engine), get_test_stream_ptr(), is_caching_test);
2324+
network->set_input_data("InputDictionary", input1);
2325+
network->set_input_data("InputText", input2);
2326+
network->set_input_data("InputUpdates", input3);
2327+
auto outputs = network->execute();
2328+
2329+
auto output = outputs.at("out").get_memory();
2330+
cldnn::mem_lock<T, mem_lock_type::read> output_ptr(output, get_test_stream());
2331+
2332+
std::vector<T> expected_results = {T(1), T(11), T(3), T(10), T(9), T(6), T(7), T(12)};
2333+
for (size_t i = 0; i < expected_results.size(); ++i) {
2334+
ASSERT_EQ(expected_results[i], output_ptr[i]) << "i=" << i << ", target_format=" << target_format;
2335+
}
2336+
}
2337+
}
2338+
2339+
TEST(scatter_update_gpu_i8, d8111_axisB) {
2340+
test_int_d8111_axisB<int8_t>(false);
2341+
}
2342+
2343+
TEST(scatter_update_gpu_u8, d8111_axisB) {
2344+
test_int_d8111_axisB<uint8_t>(false);
2345+
}

src/plugins/intel_gpu/tests/unit/test_cases/slice_gpu_test.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,4 +331,45 @@ TYPED_TEST(SliceTest, bfzyx) {
331331
this->RunAllTestCasesForParams(params);
332332
}
333333

334+
// int8 Slice: dedicated small-value case (the shared TYPED_TEST data exceeds the i8 range). Covers the
335+
// i8 output path enabled for a quantized KV cache (GroupQueryAttention int8/int4 dynamic Slice+Concat).
336+
TEST(slice_gpu_i8, bfyx) {
337+
auto& engine = get_test_engine();
338+
339+
// input [1,1,2,4] i8, slice axis 2 -> [1,1,1,4] (second row).
340+
auto input = engine.allocate_memory({ov::PartialShape{1, 1, 2, 4}, data_types::i8, format::bfyx});
341+
auto start = engine.allocate_memory({ov::PartialShape{1}, data_types::i64, format::bfyx});
342+
auto stop = engine.allocate_memory({ov::PartialShape{1}, data_types::i64, format::bfyx});
343+
auto step = engine.allocate_memory({ov::PartialShape{1}, data_types::i64, format::bfyx});
344+
auto axes = engine.allocate_memory({ov::PartialShape{1}, data_types::i64, format::bfyx});
345+
346+
set_values<int8_t>(input, {1, 2, 3, 4, -5, -6, -7, -8});
347+
set_values<int64_t>(start, {1});
348+
set_values<int64_t>(stop, {2});
349+
set_values<int64_t>(step, {1});
350+
set_values<int64_t>(axes, {2});
351+
352+
topology topology;
353+
topology.add(input_layout("input", input->get_layout()));
354+
topology.add(data("start", start));
355+
topology.add(data("stop", stop));
356+
topology.add(data("step", step));
357+
topology.add(data("axes", axes));
358+
topology.add(slice("slice", {input_info("input"), input_info("start"), input_info("stop"), input_info("step"), input_info("axes")}));
359+
360+
ExecutionConfig config = get_test_default_config(engine);
361+
config.set_property(ov::intel_gpu::allow_new_shape_infer(true));
362+
cldnn::network network(engine, topology, config);
363+
network.set_input_data("input", input);
364+
auto outputs = network.execute();
365+
366+
auto output = outputs.at("slice").get_memory();
367+
cldnn::mem_lock<int8_t, mem_lock_type::read> output_ptr(output, get_test_stream());
368+
369+
std::vector<int8_t> expected_results = {-5, -6, -7, -8};
370+
ASSERT_EQ(output_ptr.size(), expected_results.size());
371+
for (size_t i = 0; i < expected_results.size(); ++i)
372+
ASSERT_EQ(expected_results[i], output_ptr[i]) << "i=" << i;
373+
}
374+
334375
} // anonymous namespace

0 commit comments

Comments
 (0)