Skip to content

Commit 09a3f11

Browse files
authored
Fix portable dequantize_per_channel misreading Int zero points (pytorch#21773)
Differential Revision: D115185783 Pull Request resolved: pytorch#21773
1 parent 2287a8b commit 09a3f11

2 files changed

Lines changed: 87 additions & 18 deletions

File tree

‎kernels/quantized/cpu/op_dequantize.cpp‎

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,25 @@ float get_scale(const Tensor& scale, size_t channel_ix) {
167167
}
168168
}
169169

170+
/**
171+
* Reads one per-channel zero point. `dequantize_per_channel` accepts both Int
172+
* and Long zero_point tensors, so the element width must be honoured here —
173+
* reading an Int tensor as int64 reinterprets pairs of channels as one value
174+
* and runs off the end of the buffer.
175+
*/
176+
int64_t get_zero_point(const Tensor& zero_points, size_t channel_ix) {
177+
ET_CHECK_MSG(
178+
(zero_points.scalar_type() == ScalarType::Int) ||
179+
(zero_points.scalar_type() == ScalarType::Long),
180+
"zero_points.scalar_type() %" PRId8 " is not int or long type",
181+
static_cast<int8_t>(zero_points.scalar_type()));
182+
if (zero_points.scalar_type() == ScalarType::Int) {
183+
return static_cast<int64_t>(
184+
zero_points.const_data_ptr<int32_t>()[channel_ix]);
185+
}
186+
return zero_points.const_data_ptr<int64_t>()[channel_ix];
187+
}
188+
170189
bool can_use_optimized_dequantize_per_channel(
171190
const Tensor& in,
172191
const ScalarType in_dtype,
@@ -208,17 +227,15 @@ void dequantize_per_channel_optimized(
208227
}
209228
const int8_t* in_data = in.const_data_ptr<int8_t>();
210229
float* out_data = out.mutable_data_ptr<float>();
211-
const int64_t* zero_points_data = nullptr;
212-
if (opt_zero_points.has_value()) {
213-
zero_points_data = opt_zero_points.value().const_data_ptr<int64_t>();
214-
}
230+
const Tensor* zero_points =
231+
opt_zero_points.has_value() ? &opt_zero_points.value() : nullptr;
215232
const StridesType axis_stride = in.strides()[axis];
216233
const StridesType outer_stride = in.size(axis) * axis_stride;
217234
apply_over_unpacked_dim(
218235
[in_data,
219236
out_data,
220237
&scales,
221-
zero_points_data,
238+
zero_points,
222239
axis_stride,
223240
outer_stride,
224241
quant_min,
@@ -227,8 +244,8 @@ void dequantize_per_channel_optimized(
227244
const int8_t* in_data_local =
228245
in_data + outer_idx * outer_stride + unpacked_dim_idx * axis_stride;
229246
const double scale = get_scale(scales, unpacked_dim_idx);
230-
const int64_t zero_point = zero_points_data != nullptr
231-
? zero_points_data[unpacked_dim_idx]
247+
const int64_t zero_point = zero_points != nullptr
248+
? get_zero_point(*zero_points, unpacked_dim_idx)
232249
: 0;
233250
float* out_data_local = out_data + outer_idx * outer_stride +
234251
unpacked_dim_idx * axis_stride;
@@ -422,12 +439,8 @@ Tensor& dequantize_per_channel_out(
422439
dims[i] = i + 1;
423440
}
424441
}
425-
const int64_t* zero_point_data;
426-
if (opt_zero_points.has_value()) {
427-
zero_point_data = opt_zero_points.value().const_data_ptr<int64_t>();
428-
} else {
429-
zero_point_data = nullptr;
430-
}
442+
const Tensor* zero_point_tensor =
443+
opt_zero_points.has_value() ? &opt_zero_points.value() : nullptr;
431444

432445
std::optional<executorch::aten::ArrayRef<int64_t>> optional_dim_list{
433446
executorch::aten::ArrayRef<int64_t>{dims, size_t(input.dim() - 1)}};
@@ -449,14 +462,14 @@ Tensor& dequantize_per_channel_out(
449462
axis == 0, "Axis must be 0 for a single dimensional tensors"); \
450463
const std::optional<int64_t> dim; \
451464
apply_over_dim( \
452-
[input_data_ptr, out_data_ptr, zero_point_data, &scale]( \
465+
[input_data_ptr, out_data_ptr, zero_point_tensor, &scale]( \
453466
size_t numel, size_t stride, size_t base_ix) { \
454467
for (size_t i = 0; i < numel; i++) { \
455468
size_t current_ix = base_ix * stride + i; \
456469
float _scale = get_scale(scale, current_ix); \
457470
int64_t zero_point = 0; \
458-
if (zero_point_data != nullptr) { \
459-
zero_point = zero_point_data[current_ix]; \
471+
if (zero_point_tensor != nullptr) { \
472+
zero_point = get_zero_point(*zero_point_tensor, current_ix); \
460473
} \
461474
out_data_ptr[current_ix] = \
462475
static_cast<CTYPE_OUT>( \
@@ -472,8 +485,8 @@ Tensor& dequantize_per_channel_out(
472485
for (size_t channel_ix = 0; channel_ix < input.size(axis); ++channel_ix) { \
473486
float _scale = get_scale(scale, channel_ix); \
474487
int64_t _zero_point = 0; \
475-
if (zero_point_data != nullptr) { \
476-
_zero_point = zero_point_data[channel_ix]; \
488+
if (zero_point_tensor != nullptr) { \
489+
_zero_point = get_zero_point(*zero_point_tensor, channel_ix); \
477490
} \
478491
auto* out_data_ptr = out.mutable_data_ptr<CTYPE_OUT>(); \
479492
const auto* input_data_ptr = input.const_data_ptr<CTYPE_IN>(); \

‎kernels/quantized/test/op_dequantize_test.cpp‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,59 @@ TEST(OpDequantizeOutTest, DequantizePerChannelChannelsLast) {
368368

369369
EXPECT_TENSOR_CLOSE(out, expected);
370370
}
371+
372+
// The schema allows an Int zero_point tensor as well as a Long one. Reading an
373+
// Int tensor as int64 reinterprets pairs of channels as a single value and runs
374+
// off the end of the buffer, which silently corrupted every channel.
375+
template <ScalarType DTYPE>
376+
void test_per_channel_int_zero_point() {
377+
TensorFactory<DTYPE> tf;
378+
TensorFactory<ScalarType::Double> tf_double;
379+
TensorFactory<ScalarType::Int> tf_int;
380+
TensorFactory<ScalarType::Float> tfo;
381+
382+
Tensor scale = tf_double.make({4}, {0.5, 0.75, 1, 2});
383+
Tensor zero_point = tf_int.make({4}, {30, 50, 60, 90});
384+
int64_t quant_min = 0;
385+
int64_t quant_max = 127;
386+
387+
// Multi-dimensional input, channel axis 0.
388+
Tensor input = tf.full({4, 2}, 100);
389+
Tensor out = tfo.zeros({4, 2});
390+
Tensor expected = tfo.make({4, 2}, {35, 35, 37.5, 37.5, 40, 40, 20, 20});
391+
dequantize_per_channel_out(
392+
input,
393+
scale,
394+
zero_point,
395+
/*axis=*/0,
396+
quant_min,
397+
quant_max,
398+
DTYPE,
399+
optional<ScalarType>(),
400+
out);
401+
EXPECT_TENSOR_EQ(out, expected);
402+
403+
// Single-dimensional input takes a separate branch in the kernel.
404+
input = tf.make({4}, {100, 100, 100, 100});
405+
out = tfo.zeros({4});
406+
expected = tfo.make({4}, {35, 37.5, 40, 20});
407+
dequantize_per_channel_out(
408+
input,
409+
scale,
410+
zero_point,
411+
/*axis=*/0,
412+
quant_min,
413+
quant_max,
414+
DTYPE,
415+
optional<ScalarType>(),
416+
out);
417+
EXPECT_TENSOR_EQ(out, expected);
418+
}
419+
420+
TEST(OpDequantizeOutTest, DequantizePerChannelIntZeroPoint) {
421+
et_pal_init();
422+
test_per_channel_int_zero_point<ScalarType::Byte>();
423+
test_per_channel_int_zero_point<ScalarType::Char>();
424+
test_per_channel_int_zero_point<ScalarType::Short>();
425+
test_per_channel_int_zero_point<ScalarType::Int>();
426+
}

0 commit comments

Comments
 (0)