Skip to content

Commit 08e9f74

Browse files
authored
Revert "[GPU] Fix GPU DynamicQuantize batch consistency (#36078)" (#36927)
### Details Reverts #36078. #36078 narrowed the runtime skip of `DynamicQuantize` in front of `FullyConnected`. On discrete GPU the FC runs through oneDNN, which then consumes the i8 activation on every token instead of the previous f16 path. This caused a decode throughput regression on discrete GPUs (~+57% 2nd-token latency on Arc A770 for 4-bit LLMs, reported on multiple platforms/models). Integrated GPU was unaffected. Reverting to restore the previous behavior while a targeted fix is worked out.
1 parent 296f68e commit 08e9f74

7 files changed

Lines changed: 54 additions & 400 deletions

File tree

src/plugins/intel_gpu/include/intel_gpu/runtime/options.inl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ OV_CONFIG_RELEASE_INTERNAL_OPTION(ov::intel_gpu, max_kernels_per_batch, 8, "Cont
5858
OV_CONFIG_RELEASE_INTERNAL_OPTION(ov::intel_gpu, impls_cache_capacity, 300, "Controls capacity of LRU implementations cache that is created for each program object for dynamic models")
5959
OV_CONFIG_RELEASE_INTERNAL_OPTION(ov::intel_gpu, asym_dynamic_quantization, false, "Enforce asymmetric mode for dynamically quantized activations")
6060
OV_CONFIG_RELEASE_INTERNAL_OPTION(ov::intel_gpu, could_use_flashattn_v2, true, "Enable/Disable SDPA primitive executing with FlashAttenV2 online softmax tricks.")
61-
OV_CONFIG_RELEASE_INTERNAL_OPTION(ov::intel_gpu, dynamic_quantization_threshold, 64, "Skip runtime DynamicQuantize for batch sizes <= this value only when integrated-GPU FC can execute an equivalent internal dynamic-quantized path. 0 disables threshold-based skipping")
61+
OV_CONFIG_RELEASE_INTERNAL_OPTION(ov::intel_gpu, dynamic_quantization_threshold, 64, "Apply dynamic quantization only when batch size is larger than this value in OneDNN")
6262
OV_CONFIG_RELEASE_INTERNAL_OPTION(ov::intel_gpu, dynamic_quantization_precomputed_reduction, true, "Precompute reduction of activation for faster dynamic quantization in case of asymmetric weight")
6363
OV_CONFIG_RELEASE_INTERNAL_OPTION(ov::intel_gpu, allow_bypass_xattn, true, "Allow bypass xattn execution if threshold >= 1.0.")
6464
OV_CONFIG_RELEASE_INTERNAL_OPTION(ov::intel_gpu, weightless_attr, nullptr, "Used to configure ov::WeightlessCacheAttribute for constants that are not loaded from a .bin file. This typically applies to non-IR inputs (e.g., ORT)")

src/plugins/intel_gpu/src/graph/dynamic_quantize.cpp

Lines changed: 5 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -4,167 +4,17 @@
44

55
#include "ov_ops/dynamic_quantize.hpp"
66
#include "dynamic_quantize_inst.h"
7-
#include "fully_connected/fully_connected_kernel_bf_tiled.h"
87
#include "fully_connected_inst.h"
98

109
#include "primitive_type_base.h"
1110
#include "json_object.h"
12-
#include <limits>
1311
#include <string>
1412

1513
namespace cldnn {
1614
GPU_DEFINE_PRIMITIVE_TYPE_ID(dynamic_quantize);
1715

18-
static size_t get_static_last_dim(const layout& layout) {
19-
const auto& shape = layout.get_partial_shape();
20-
if (shape.rank().is_dynamic() || shape.size() == 0 || shape[shape.size() - 1].is_dynamic()) {
21-
return 0;
22-
}
23-
24-
return static_cast<size_t>(shape[shape.size() - 1].get_length());
25-
}
26-
27-
static bool has_fc_decompression_zp(const fully_connected_node& fc_node) {
28-
const auto& fc_prim = fc_node.get_primitive();
29-
return fc_prim->decompression_zero_point.is_valid() ||
30-
fc_prim->decompression_zero_point_scalar.has_value();
31-
}
32-
33-
static size_t get_fc_scale_group_size(const fully_connected_node& fc_node) {
34-
const auto& fc_prim = fc_node.get_primitive();
35-
if (!fc_prim->decompression_scale.is_valid()) {
36-
return 0;
37-
}
38-
39-
const size_t scale_dep_idx = 2 + (fc_prim->bias.is_valid() ? 1 : 0);
40-
if (fc_node.get_dependencies().size() <= scale_dep_idx) {
41-
return 0;
42-
}
43-
44-
const auto ifm_size = get_static_last_dim(fc_node.weights().get_output_layout());
45-
const auto scale_groups = get_static_last_dim(fc_node.get_dependency(scale_dep_idx).get_output_layout());
46-
if (ifm_size == 0 || scale_groups == 0 || ifm_size % scale_groups != 0) {
47-
return 0;
48-
}
49-
50-
return ifm_size / scale_groups;
51-
}
52-
53-
static size_t get_fc_zp_group_size(const fully_connected_node& fc_node) {
54-
const auto& fc_prim = fc_node.get_primitive();
55-
if (fc_prim->decompression_zero_point_scalar.has_value()) {
56-
return get_static_last_dim(fc_node.weights().get_output_layout());
57-
}
58-
59-
if (!fc_prim->decompression_zero_point.is_valid()) {
60-
return 0;
61-
}
62-
63-
const size_t zp_dep_idx = 2 + (fc_prim->bias.is_valid() ? 1 : 0) + 1;
64-
if (fc_node.get_dependencies().size() <= zp_dep_idx) {
65-
return 0;
66-
}
67-
68-
const auto ifm_size = get_static_last_dim(fc_node.weights().get_output_layout());
69-
const auto zp_groups = get_static_last_dim(fc_node.get_dependency(zp_dep_idx).get_output_layout());
70-
if (ifm_size == 0 || zp_groups == 0 || ifm_size % zp_groups != 0) {
71-
return 0;
72-
}
73-
74-
return ifm_size / zp_groups;
75-
}
76-
77-
static bool is_fc_bf_tiled_kernel(const std::string& kernel_name) {
78-
return kernel_name == "fully_connected_gpu_bf_tiled";
79-
}
80-
81-
static kernel_selector::dev_type to_kernel_selector_device_type(device_type type) {
82-
return type == device_type::discrete_gpu ? kernel_selector::dev_type::discrete_gpu
83-
: kernel_selector::dev_type::integrated_gpu;
84-
}
85-
86-
static bool can_skip_for_fully_connected(const dynamic_quantize_node& node,
87-
const fully_connected_node& fc_node,
88-
const layout& act_layout,
89-
size_t input_batch) {
90-
const auto& attrs = node.get_primitive()->attrs;
91-
92-
if (attrs.quantization_type != ov::op::internal::DynamicQuantize::QuantizationType::Symmetric ||
93-
attrs.quantization_dt != ov::element::i8 ||
94-
attrs.scale_dt != ov::element::f16 ||
95-
attrs.precomputed_reduction ||
96-
attrs.group_sizes.empty() ||
97-
attrs.group_sizes.back() == std::numeric_limits<uint64_t>::max()) {
98-
return false;
99-
}
100-
101-
for (size_t i = 0; i + 1 < attrs.group_sizes.size(); ++i) {
102-
if (attrs.group_sizes[i] != 1) {
103-
return false;
104-
}
105-
}
106-
107-
if (act_layout.data_type != data_types::f16) {
108-
return false;
109-
}
110-
111-
bool has_equivalent_fc_fast_path = false;
112-
const auto& forced_impls = fc_node.get_program().get_config().get_force_implementations();
113-
auto forced_impl = forced_impls.find(fc_node.id());
114-
if (forced_impl != forced_impls.end()) {
115-
if (forced_impl->second.impl_type != impl_types::ocl) {
116-
return false;
117-
}
118-
if (!forced_impl->second.kernel_name.empty() && !is_fc_bf_tiled_kernel(forced_impl->second.kernel_name)) {
119-
return false;
120-
}
121-
has_equivalent_fc_fast_path = is_fc_bf_tiled_kernel(forced_impl->second.kernel_name);
122-
}
123-
124-
const auto* fc_impl = fc_node.get_selected_impl();
125-
if (fc_impl != nullptr) {
126-
if (fc_impl->is_onednn()) {
127-
return false;
128-
}
129-
has_equivalent_fc_fast_path |= is_fc_bf_tiled_kernel(fc_impl->get_kernel_name());
130-
}
131-
132-
if (fc_node.get_preferred_impl_type() == impl_types::onednn && !has_equivalent_fc_fast_path) {
133-
return false;
134-
}
135-
136-
const auto input_features = get_static_last_dim(act_layout);
137-
const auto weight_layout = fc_node.weights().get_output_layout();
138-
const auto weight_ifm = get_static_last_dim(weight_layout);
139-
if (input_features == 0 || weight_ifm == 0 || input_features != weight_ifm) {
140-
return false;
141-
}
142-
143-
const auto weight_type = weight_layout.data_type;
144-
const bool has_zp = has_fc_decompression_zp(fc_node);
145-
const bool is_4bit_weight = weight_type == data_types::i4 || weight_type == data_types::u4;
146-
const bool is_8bit_asym_weight = weight_type == data_types::u8 && has_zp;
147-
const auto& device_info = fc_node.get_program().get_engine().get_device_info();
148-
149-
kernel_selector::fc_kernel_bf_tiled_utils::slm_dq_eligibility_params eligibility;
150-
eligibility.device_type = to_kernel_selector_device_type(device_info.dev_type);
151-
eligibility.max_local_mem_size = device_info.max_local_mem_size;
152-
eligibility.is_4bit_weight = is_4bit_weight;
153-
eligibility.is_8bit_asym_weight = is_8bit_asym_weight;
154-
eligibility.weight_ifm = weight_ifm;
155-
eligibility.scale_group_size = get_fc_scale_group_size(fc_node);
156-
eligibility.zp_group_size = get_fc_zp_group_size(fc_node);
157-
eligibility.has_decompression_zp = has_zp;
158-
eligibility.dq_group_size = fc_node.get_program().get_config().get_dynamic_quantization_group_size();
159-
160-
return has_equivalent_fc_fast_path &&
161-
kernel_selector::fc_kernel_bf_tiled_utils::would_use_slm_with_internal_dq(eligibility,
162-
input_batch,
163-
attrs.group_sizes.back());
164-
}
165-
166-
// can_be_optimized flag will be turned on from primitive_inst::update_shape function only when the
167-
// following primitive can reproduce DynamicQuantize semantics internally for the current shape.
16+
// We should skip dynamic_quantization execution for 2nd token of LLM because it does not show performance gain.
17+
// can_be_optimized flag will be turned on from primitive_inst::update_shape function
16818
static bool should_skip_execution(const dynamic_quantize_node& node, const layout& act_layout, const dynamic_quantize::Attributes& attrs) {
16919
if (cldnn::data_type_traits::is_floating_point(attrs.quantization_dt)) {
17020
return false; // Execute unconditionally for floating point types.
@@ -180,22 +30,16 @@ static bool should_skip_execution(const dynamic_quantize_node& node, const layou
18030
if (!(*node.get_users().begin())->is_type<fully_connected>())
18131
return false;
18232

33+
// If batch size is 1, dynamic_quantize is disabled for performance reason
18334
size_t input_batch = act_layout.batch();
18435
if (act_layout.format == format::bfyx && act_layout.get_partial_shape().size() != 2) {
18536
// 3D input
18637
input_batch = act_layout.batch() * act_layout.feature();
18738
}
18839

189-
auto& fc_user = (*node.get_users().begin())->as<fully_connected>();
190-
if (!can_skip_for_fully_connected(node, fc_user, act_layout, input_batch)) {
191-
GPU_DEBUG_TRACE << node.id() << " dyn_quan is not runtime-skipped: no equivalent FC fast path" << std::endl;
192-
return false;
193-
}
194-
195-
const auto dynamic_quantization_threshold = node.get_program().get_config().get_dynamic_quantization_threshold();
196-
if (dynamic_quantization_threshold != 0 && dynamic_quantization_threshold >= input_batch) {
40+
if (node.get_program().get_config().get_dynamic_quantization_threshold() >= input_batch) {
19741
GPU_DEBUG_TRACE << node.id() << " dyn_quan is turned off: input batch size is too small - " << input_batch << " / "
198-
<< dynamic_quantization_threshold << std::endl;
42+
<< node.get_program().get_config().get_dynamic_quantization_threshold() << std::endl;
19943
return true;
20044
}
20145

src/plugins/intel_gpu/src/kernel_selector/cl_kernels/fully_connected_gpu_bf_tiled.cl

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,28 +36,29 @@ KERNEL(quantize_input)(
3636
max[i] = fmax(fmax(fabs(input_0[0]), fabs(input_0[1])), fmax(fabs(input_0[2]), fabs(input_0[3])));
3737
}
3838

39-
INPUT0_TYPE max_value = 0.003h;
39+
INPUT0_TYPE max_value = 0.001h;
4040
for (uint i = 0 ; i < quantize_block ; i+=8) {
4141
INPUT0_TYPE temp = fmax(fmax(fmax(max[i], max[i+1]), fmax(max[i+2], max[i+3])),
4242
fmax(fmax(max[i+4], max[i+5]), fmax(max[i+6], max[i+7])));
4343
max_value = fmax(max_value, temp);
4444
}
4545

46-
INPUT0_TYPE inv_quan_scale = 127.0h / max_value;
46+
float quan_scale = (float)max_value / 127.f;
4747
#if COMPRESSED_WEIGHTS_INT8
4848
int quantized_sum = 0;
4949
#endif
5050
for (uint i = 0 ; i < quantize_block ; ++i) {
5151
input_0 = vload4(0, &input[input_offset + i * 4]);
52-
quantized_value = CAT(CAT(convert_, MAKE_VECTOR_TYPE(DQ_TYPE, INPUT_LOAD_SIZE)), _rte)(input_0 * inv_quan_scale);
52+
float4 buff = convert_float4(input_0) / quan_scale;
53+
quantized_value = CAT(CAT(convert_, MAKE_VECTOR_TYPE(DQ_TYPE, INPUT_LOAD_SIZE)), _rte)(buff);
5354
#if COMPRESSED_WEIGHTS_INT8
5455
quantized_sum += quantized_value[0] + quantized_value[1] + quantized_value[2] + quantized_value[3];
5556
#endif
5657
vstore4(quantized_value, 0, &quantized_input[input_offset + i * 4]);
5758
}
5859

59-
// Pair of dequantization scale and quantized activation_sum for each group
60-
quan_var[offset * 2] = convert_half(1.0h / inv_quan_scale);
60+
// Pair of quantizing_scale and quantized activation_sum for each group
61+
quan_var[offset * 2] = convert_half(quan_scale);
6162
#if COMPRESSED_WEIGHTS_INT8
6263
quan_var[(offset * 2) + 1] = convert_half(quantized_sum);
6364
#endif

src/plugins/intel_gpu/src/kernel_selector/kernels/fully_connected/fully_connected_kernel_bf_tiled.cpp

Lines changed: 38 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ static constexpr size_t simd = 16;
1515
static constexpr size_t input_load_size = 4;
1616
static constexpr size_t min_quantize_grp_size = (simd * 2); // SIMD * (min value of tile_ifm)
1717
static constexpr size_t min_slm_size = 256;
18+
static std::vector<size_t> available_quantize_grp_size = {128, 64, 32};
1819

1920
// Per-direction bitmasks of batch sizes where dyn_b is beneficial (based on MTL profiling).
2021
// IFM << OFM (e.g. 3584→18944): exclude {20, 24}
@@ -95,14 +96,13 @@ bool is_8bit_asym_wei(const fully_connected_params& params) {
9596

9697
bool is_weight_dyn_quantizable(const fully_connected_params& params) {
9798
auto weight_type = params.weights.GetDType();
98-
const bool is_4bit_weight = weight_type == WeightsType::INT4 || weight_type == WeightsType::UINT4;
99-
100-
return is_weight_dyn_quantizable(is_4bit_weight, is_8bit_asym_wei(params));
101-
}
99+
if (weight_type == WeightsType::INT4 || weight_type == WeightsType::UINT4)
100+
return true;
101+
// No validated case of sym 8bit weight
102+
if (is_8bit_asym_wei(params))
103+
return true;
102104

103-
bool is_weight_dyn_quantizable(bool is_4bit_weight, bool is_8bit_asym_weight) {
104-
// No validated case of sym 8bit weight.
105-
return is_4bit_weight || is_8bit_asym_weight;
105+
return false;
106106
}
107107

108108
bool is_per_token_dynamic_quantize(const fully_connected_params& params) {
@@ -114,90 +114,54 @@ bool is_per_token_dynamic_quantize(const fully_connected_params& params) {
114114
}
115115

116116
// DYNAMIC_QUANTIZE
117-
size_t get_dynamic_quantize_group_size(size_t requested_group_size,
118-
size_t scale_group_size,
119-
size_t zp_group_size,
120-
bool has_decompression_zp,
121-
bool is_8bit_asym_weight) {
122-
if (requested_group_size == UINT64_MAX) {
117+
size_t get_dynamic_quantize_group_size(const fully_connected_params& params) {
118+
auto dynamic_quantization_group_size = params.dynamic_quantization_group_size;
119+
120+
size_t scale_group_size = get_scale_group_size(params);
121+
size_t zp_group_num = params.decompression_zero_point.Feature().v;
122+
size_t zp_group_size = 0;
123+
if (params.has_decompression_zp)
124+
zp_group_size = params.weights.IFM().v / params.decompression_zero_point.Feature().v;
125+
126+
// Per-token dyn-quan
127+
if (dynamic_quantization_group_size >= min_quantize_grp_size && is_per_token_dynamic_quantize(params)) {
128+
// Validate size to fit dyn-quan group to the size of weight-scale and weight-zp
123129
if ((scale_group_size % min_quantize_grp_size) == 0 && scale_group_size > min_quantize_grp_size) {
124-
auto group_size = scale_group_size;
130+
dynamic_quantization_group_size = scale_group_size;
125131

126132
// For int8 ASYM model, activation_sum should fit to weight zp
127-
if (is_8bit_asym_weight && has_decompression_zp &&
128-
group_size > zp_group_size && (zp_group_size % input_load_size) == 0) {
129-
group_size = zp_group_size;
133+
if (is_8bit_asym_wei(params) && params.has_decompression_zp == true &&
134+
dynamic_quantization_group_size > zp_group_size && (zp_group_size % input_load_size) == 0) {
135+
dynamic_quantization_group_size = zp_group_size;
130136
}
131137

132-
return group_size;
138+
GPU_DEBUG_LOG << "FC dyn-quantize by per-token. Actual dyn_quan_group_size("
139+
<< dynamic_quantization_group_size << ") : From scale_group_size (" << scale_group_size
140+
<< ", zp_group_size(" << zp_group_size << "), zp_group_num(" << zp_group_num
141+
<< "), ifm_size (" << get_input_bf_size(params).second << ")" << std::endl;
142+
return (size_t)dynamic_quantization_group_size;
133143
}
134144
}
135145

136-
// Grouped-size dyn-quan : use aligned sizes which are in descending priority order.
137-
constexpr size_t available_quantize_grp_size[] = {128, 64, 32};
146+
// Grouped-size dyn-quan : use aligned sizes which are in 'available_quantize_grp_size'
138147
for (auto group_size : available_quantize_grp_size) {
139-
if (requested_group_size >= group_size && (scale_group_size % group_size) == 0) {
140-
if (group_size > scale_group_size) {
141-
return scale_group_size;
148+
if (dynamic_quantization_group_size >= group_size && (scale_group_size % group_size) == 0) {
149+
dynamic_quantization_group_size = group_size;
150+
151+
if (dynamic_quantization_group_size > scale_group_size) {
152+
GPU_DEBUG_TRACE_DETAIL << " Scale group size " << scale_group_size
153+
<< " is smaller than FC dyn-quan group size " << dynamic_quantization_group_size
154+
<< ". Reduce FC dyn-quan group size to scale size." << std::endl;
155+
dynamic_quantization_group_size = scale_group_size;
142156
}
143157

144-
return group_size;
158+
return (size_t)dynamic_quantization_group_size;
145159
}
146160
}
147161

148162
return 0;
149163
}
150164

151-
size_t get_dynamic_quantize_group_size(const fully_connected_params& params) {
152-
size_t scale_group_size = get_scale_group_size(params);
153-
size_t zp_group_num = params.decompression_zero_point.Feature().v;
154-
size_t zp_group_size = 0;
155-
if (params.has_decompression_zp)
156-
zp_group_size = params.weights.IFM().v / params.decompression_zero_point.Feature().v;
157-
158-
const auto dynamic_quantization_group_size = get_dynamic_quantize_group_size(params.dynamic_quantization_group_size,
159-
scale_group_size,
160-
zp_group_size,
161-
params.has_decompression_zp,
162-
is_8bit_asym_wei(params));
163-
if (is_per_token_dynamic_quantize(params) && dynamic_quantization_group_size != 0) {
164-
GPU_DEBUG_LOG << "FC dyn-quantize by per-token. Actual dyn_quan_group_size("
165-
<< dynamic_quantization_group_size << ") : From scale_group_size (" << scale_group_size
166-
<< ", zp_group_size(" << zp_group_size << "), zp_group_num(" << zp_group_num
167-
<< "), ifm_size (" << get_input_bf_size(params).second << ")" << std::endl;
168-
}
169-
170-
return dynamic_quantization_group_size;
171-
}
172-
173-
bool would_use_slm_with_internal_dq(const slm_dq_eligibility_params& params,
174-
size_t runtime_batch,
175-
size_t standalone_dq_group_size) {
176-
if (params.device_type != dev_type::integrated_gpu)
177-
return false;
178-
179-
if (!is_weight_dyn_quantizable(params.is_4bit_weight, params.is_8bit_asym_weight))
180-
return false;
181-
182-
constexpr size_t min_required_slm = 2 * simd * 1 * simd * 2;
183-
if (params.max_local_mem_size < min_required_slm)
184-
return false;
185-
186-
constexpr size_t default_alignment = 16;
187-
if (runtime_batch + default_alignment <= min_slm_size)
188-
return false;
189-
190-
const auto fc_internal_group_size = get_dynamic_quantize_group_size(params.dq_group_size,
191-
params.scale_group_size,
192-
params.zp_group_size,
193-
params.has_decompression_zp,
194-
params.is_8bit_asym_weight);
195-
return fc_internal_group_size != 0 &&
196-
params.weight_ifm != 0 &&
197-
params.weight_ifm % fc_internal_group_size == 0 &&
198-
fc_internal_group_size == standalone_dq_group_size;
199-
}
200-
201165
bool should_dynamic_quantize(const fully_connected_params& params) {
202166
size_t dynamic_quantization_group_size = get_dynamic_quantize_group_size(params);
203167

0 commit comments

Comments
 (0)