Skip to content

Commit 06c5a72

Browse files
authored
Define quantization for non-finite values (pytorch#22811)
Summary: Host replay sanitizers exposed undefined behavior when non-finite quantization results were converted to integers before clamping. Define deterministic behavior: NaN maps to the clamped zero point, positive infinity to quant_max, and negative infinity to quant_min. Clamp before integer conversion and keep scalar and ARM exceptional-value handling consistent, with regression coverage for non-finite inputs, finite overflow, zero-point clamping, and per-channel quantization. Adapted from the repair proposed in D119101166. Edits were made in fbcode; the repository commit hook mirrored them into xplat, and the mirrored changes are retained. Authored with assistance from Devmate. Reviewed By: rascani Differential Revision: D120017028 Pull Request resolved: pytorch#22811
1 parent bbc6f33 commit 06c5a72

2 files changed

Lines changed: 150 additions & 24 deletions

File tree

‎kernels/quantized/cpu/op_quantize.cpp‎

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -92,22 +92,38 @@ void check_quantize_per_tensor_args(
9292

9393
} // namespace
9494

95+
template <typename T, typename K>
96+
T quantize_val_with_inv_scale(
97+
float inv_scale,
98+
int64_t zero_point,
99+
K value,
100+
int64_t quant_min,
101+
int64_t quant_max) {
102+
const float scaled_value = static_cast<float>(inv_scale * value);
103+
// Map NaN to the zero point and clamp infinities before integer conversion.
104+
// Double precision keeps int32 bounds exactly representable while clamping.
105+
const double qvalue = std::isnan(scaled_value)
106+
? static_cast<double>(zero_point)
107+
: static_cast<double>(zero_point) + std::nearbyint(scaled_value);
108+
const double clamped_value = std::min(
109+
static_cast<double>(quant_max),
110+
std::max(static_cast<double>(quant_min), qvalue));
111+
return static_cast<T>(clamped_value);
112+
}
113+
95114
template <typename T, typename K>
96115
T quantize_val(
97116
double scale,
98117
int64_t zero_point,
99118
K value,
100119
int64_t quant_min,
101120
int64_t quant_max) {
102-
int64_t qvalue;
103-
float inv_scale = 1.0f / static_cast<float>(scale);
104-
qvalue = static_cast<int64_t>(
105-
static_cast<int32_t>(zero_point) +
106-
std::nearbyint(static_cast<float>(inv_scale * value)));
107-
108-
qvalue = std::max<int64_t>(qvalue, quant_min);
109-
qvalue = std::min<int64_t>(qvalue, quant_max);
110-
return static_cast<T>(qvalue);
121+
return quantize_val_with_inv_scale<T>(
122+
1.0f / static_cast<float>(scale),
123+
zero_point,
124+
value,
125+
quant_min,
126+
quant_max);
111127
}
112128

113129
#if defined(__aarch64__) || defined(__ARM_NEON__)
@@ -180,7 +196,8 @@ void quantize_arm(
180196
const float32x4_t vin0123 = vld1q_f32(in + i);
181197
const float32x4_t vin4567 = vld1q_f32(in + i + 4);
182198

183-
// Multiply by inv_scale and round
199+
// FCVTNS maps NaN to zero and saturates infinities before adding
200+
// zero_point.
184201
const int32x4_t v0123_rounded =
185202
vcvtnq_s32_f32(vmulq_f32(vin0123, vinv_scale));
186203
const int32x4_t v4567_rounded =
@@ -201,31 +218,41 @@ void quantize_arm(
201218

202219
// Handle remaining elements with proper quant_min/quant_max clamping
203220
for (; i < N; ++i) {
204-
float val = in[i] * inv_scale;
205-
int32_t qval = static_cast<int32_t>(std::nearbyint(val)) + zero_point;
206-
qval = std::max(quant_min, std::min(quant_max, qval));
207-
out[i] = static_cast<T>(qval);
221+
out[i] = quantize_val_with_inv_scale<T>(
222+
inv_scale, zero_point, in[i], quant_min, quant_max);
208223
}
209224

210225
#else
211226
// ARMv7: Use magic float rounding
212227
const int32x4_t voffset = vdupq_n_s32(zero_point - 0x4B400000);
213228
const float32x4_t vmagic_float = vdupq_n_f32(12582912.0f);
229+
const float32x4_t vmagic_float_maximum = vdupq_n_f32(4194304.0f);
214230

215231
int64_t i = 0;
216232
// Process 8 elements at a time
217233
for (; i + 8 <= N; i += 8) {
218234
const float32x4_t vin0123 = vld1q_f32(in + i);
219235
const float32x4_t vin4567 = vld1q_f32(in + i + 4);
220236

237+
const float32x4_t vscaled0123 = vmulq_f32(vin0123, vinv_scale);
238+
const float32x4_t vscaled4567 = vmulq_f32(vin4567, vinv_scale);
239+
const uint32x4_t valid = vandq_u32(
240+
vcleq_f32(vabsq_f32(vscaled0123), vmagic_float_maximum),
241+
vcleq_f32(vabsq_f32(vscaled4567), vmagic_float_maximum));
242+
const uint32x2_t valid_pairs =
243+
vand_u32(vget_low_u32(valid), vget_high_u32(valid));
244+
if (vget_lane_u32(vpmin_u32(valid_pairs, valid_pairs), 0) != UINT32_MAX) {
245+
for (int64_t j = 0; j < 8; ++j) {
246+
out[i + j] = quantize_val_with_inv_scale<T>(
247+
inv_scale, zero_point, in[i + j], quant_min, quant_max);
248+
}
249+
continue;
250+
}
251+
221252
const int32x4_t vraw0123 = vaddq_s32(
222-
voffset,
223-
vreinterpretq_s32_f32(
224-
vaddq_f32(vmagic_float, vmulq_f32(vin0123, vinv_scale))));
253+
voffset, vreinterpretq_s32_f32(vaddq_f32(vmagic_float, vscaled0123)));
225254
const int32x4_t vraw4567 = vaddq_s32(
226-
voffset,
227-
vreinterpretq_s32_f32(
228-
vaddq_f32(vmagic_float, vmulq_f32(vin4567, vinv_scale))));
255+
voffset, vreinterpretq_s32_f32(vaddq_f32(vmagic_float, vscaled4567)));
229256

230257
const int16x8_t vraw01234567 =
231258
vcombine_s16(vqmovn_s32(vraw0123), vqmovn_s32(vraw4567));
@@ -237,10 +264,8 @@ void quantize_arm(
237264

238265
// Handle remaining elements with proper quant_min/quant_max clamping
239266
for (; i < N; ++i) {
240-
float val = in[i] * inv_scale;
241-
int32_t qval = static_cast<int32_t>(std::nearbyint(val)) + zero_point;
242-
qval = std::max(quant_min, std::min(quant_max, qval));
243-
out[i] = static_cast<T>(qval);
267+
out[i] = quantize_val_with_inv_scale<T>(
268+
inv_scale, zero_point, in[i], quant_min, quant_max);
244269
}
245270
#endif
246271
}

‎kernels/quantized/test/op_quantize_test.cpp‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <executorch/test/utils/DeathTest.h>
1515

1616
#include <gtest/gtest.h>
17+
#include <limits>
1718

1819
using namespace ::testing;
1920
using executorch::aten::ScalarType;
@@ -182,6 +183,106 @@ TEST(OpQuantizeOutTest, TestOutOfBounds) {
182183
EXPECT_TENSOR_EQ(out, expected);
183184
}
184185

186+
template <ScalarType INPUT_DTYPE, ScalarType OUTPUT_DTYPE>
187+
void test_non_finite_values() {
188+
TensorFactory<INPUT_DTYPE> tf;
189+
TensorFactory<OUTPUT_DTYPE> tfo;
190+
const float nan = std::numeric_limits<float>::quiet_NaN();
191+
const float inf = std::numeric_limits<float>::infinity();
192+
// One SIMD block and a remainder containing each non-finite value.
193+
Tensor input =
194+
tf.make({11}, {nan, inf, -inf, 0, 1, -1, 0.25, 0.75, nan, inf, -inf});
195+
Tensor out = tfo.zeros({11});
196+
Tensor expected = tfo.make({11}, {5, 10, 0, 5, 7, 3, 5, 7, 5, 10, 0});
197+
198+
quantize_per_tensor_out(input, 0.5, 5, 0, 10, OUTPUT_DTYPE, out);
199+
200+
EXPECT_TENSOR_EQ(out, expected);
201+
}
202+
203+
TEST(OpQuantizeOutTest, NonFiniteValuesHaveDeterministicOutputs) {
204+
test_non_finite_values<ScalarType::Float, ScalarType::Char>();
205+
test_non_finite_values<ScalarType::Float, ScalarType::Byte>();
206+
test_non_finite_values<ScalarType::Float, ScalarType::Short>();
207+
test_non_finite_values<ScalarType::Float, ScalarType::UInt16>();
208+
test_non_finite_values<ScalarType::Float, ScalarType::Bits16>();
209+
test_non_finite_values<ScalarType::Float, ScalarType::Int>();
210+
test_non_finite_values<ScalarType::Half, ScalarType::Char>();
211+
test_non_finite_values<ScalarType::Double, ScalarType::Char>();
212+
}
213+
214+
TEST(OpQuantizeOutTest, NaNMapsToClampedZeroPoint) {
215+
TensorFactory<ScalarType::Float> tf;
216+
TensorFactory<ScalarType::Char> tfo;
217+
Tensor input = tf.full({9}, std::numeric_limits<float>::quiet_NaN());
218+
Tensor out = tfo.zeros({9});
219+
220+
for (const int64_t zero_point : {-20, 20}) {
221+
SCOPED_TRACE(zero_point);
222+
Tensor expected = tfo.full({9}, zero_point < 0 ? -10 : 10);
223+
quantize_per_tensor_out(
224+
input, 0.5, zero_point, -10, 10, ScalarType::Char, out);
225+
EXPECT_TENSOR_EQ(out, expected);
226+
}
227+
}
228+
229+
TEST(OpQuantizeOutTest, LargeFiniteValuesSaturateBeforeIntegerConversion) {
230+
TensorFactory<ScalarType::Float> tf;
231+
const float largest = std::numeric_limits<float>::max();
232+
Tensor input = tf.make(
233+
{10},
234+
{largest,
235+
-largest,
236+
1e20f,
237+
-1e20f,
238+
1e10f,
239+
-1e10f,
240+
0,
241+
1,
242+
largest,
243+
-largest});
244+
TensorFactory<ScalarType::Char> tf_char;
245+
Tensor out_char = tf_char.zeros({10});
246+
Tensor expected_char =
247+
tf_char.make({10}, {10, -10, 10, -10, 10, -10, 5, 7, 10, -10});
248+
249+
quantize_per_tensor_out(input, 0.5, 5, -10, 10, ScalarType::Char, out_char);
250+
EXPECT_TENSOR_EQ(out_char, expected_char);
251+
252+
TensorFactory<ScalarType::Int> tf_int;
253+
constexpr int32_t min = std::numeric_limits<int32_t>::min();
254+
constexpr int32_t max = std::numeric_limits<int32_t>::max();
255+
Tensor out_int = tf_int.zeros({10});
256+
Tensor expected_int =
257+
tf_int.make({10}, {max, min, max, min, max, min, 5, 7, max, min});
258+
259+
quantize_per_tensor_out(input, 0.5, 5, min, max, ScalarType::Int, out_int);
260+
EXPECT_TENSOR_EQ(out_int, expected_int);
261+
}
262+
263+
TEST(OpQuantizeOutTest, PerChannelNonFiniteValuesHaveDeterministicOutputs) {
264+
TensorFactory<ScalarType::Float> tf;
265+
TensorFactory<ScalarType::Double> tf_double;
266+
TensorFactory<ScalarType::Long> tf_long;
267+
TensorFactory<ScalarType::Char> tfo;
268+
const float nan = std::numeric_limits<float>::quiet_NaN();
269+
const float inf = std::numeric_limits<float>::infinity();
270+
Tensor input =
271+
tf.make({2, 11}, {nan, inf, -inf, 0, 1, -1, 0.25, 0.75, nan, inf, -inf,
272+
nan, inf, -inf, 0, 1, -1, 0.25, 0.75, nan, inf, -inf});
273+
Tensor scale = tf_double.make({2}, {0.5, 1.0});
274+
Tensor zero_point = tf_long.make({2}, {5, -5});
275+
Tensor out = tfo.zeros({2, 11});
276+
Tensor expected =
277+
tfo.make({2, 11}, {5, 10, -10, 5, 7, 3, 5, 7, 5, 10, -10,
278+
-5, 10, -10, -5, -4, -6, -5, -4, -5, 10, -10});
279+
280+
quantize_per_channel_out(
281+
input, scale, zero_point, 0, -10, 10, ScalarType::Char, out);
282+
283+
EXPECT_TENSOR_EQ(out, expected);
284+
}
285+
185286
TEST(OpQuantizeOutTest, QuantizePerChannel) {
186287
TensorFactory<ScalarType::Float> tf_float;
187288
TensorFactory<ScalarType::Double> tf_double;

0 commit comments

Comments
 (0)