Skip to content

Commit 64c1675

Browse files
mvartani-metafacebook-github-bot
authored andcommitted
Support int16 in HiFi quantized_layer_norm (#24091)
Summary: The HiFi quantized_layer_norm kernels dispatched with ET_FORALL_CADENCE_QUANTIZED_TYPES, so int16 inputs fell through to ET_DCHECK_MSG. That check compiles out in release builds, which left the output tensor unwritten and produced garbage numerics for int16 models. Simply widening the dispatch is not enough because the statistics used int32 accumulators. For int16, a single squared value can reach 2^30, so sq_sum overflows after two elements. The variance was also computed as E[x^2] - E[x]^2 in fp32, which cancels catastrophically when the mean is large relative to the spread. Changes: - Dispatch both overloads with ET_FORALL_CADENCE_QUANTIZED_TYPES_WITH_INT16. - Accumulate sum and sq_sum in int64 for 16-bit types. 8-bit types keep int32 in the inner loop. - Widen to the accumulator type before squaring, so the product cannot overflow int32. - Apply the zero-point correction in int64 and compute the variance numerator N*sum(v^2) - sum(v)^2 exactly in integers before converting to float. For 8-bit this is equal or more accurate than before. - Declare quantized_layer_norm_out and quantized_layer_norm_per_tensor_out in operators.h and add a gtest. Reviewed By: DrJessop Differential Revision: D126939431
1 parent 6368414 commit 64c1675

5 files changed

Lines changed: 210 additions & 18 deletions

File tree

‎backends/cadence/hifi/operators/op_quantized_layer_norm.cpp‎

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
#include <executorch/runtime/kernel/kernel_includes.h>
1212
#include <algorithm>
1313
#include <cmath>
14+
#include <cstdint>
1415
#include <tuple>
16+
#include <type_traits>
1517

1618
using ::executorch::aten::IntArrayRef;
1719
using ::executorch::aten::ScalarType;
@@ -55,32 +57,41 @@ void quantized_layer_norm_per_tensor_(
5557
const T* __restrict__ x = in_data + i * last_dim;
5658
T* __restrict__ y = out_data + i * last_dim;
5759

58-
// compute sum and squared sum. The fp32 sum can be approximated as:
59-
// (X_1 - in_zero_point) * in_scale + (X_2 - in_zero_point) * in_scale + ...
60-
// (X_N - in_zero_point) * in_scale.
61-
int32_t sum = 0;
62-
int32_t sq_sum = last_dim * input_zero_point * input_zero_point;
60+
// 16-bit squares overflow an int32 sum after two elements.
61+
using acc_t = std::conditional_t<sizeof(T) == 1, int32_t, int64_t>;
62+
acc_t sum = 0;
63+
acc_t sq_sum = 0;
6364
#pragma simd
6465
for (size_t j = 0; j < last_dim; ++j) {
65-
int32_t val = x[j];
66+
const int32_t val = x[j];
6667
sum += val;
67-
sq_sum += val * val;
68+
sq_sum += static_cast<acc_t>(val) * val;
6869
}
69-
sq_sum -= (2 * sum * input_zero_point);
70-
sum -= (last_dim * input_zero_point);
71-
72-
float mean = XT_DIV_S(XT_MUL_S(input_scale, sum), last_dim);
73-
float variance =
74-
XT_DIV_S(
75-
XT_MUL_S(sq_sum, XT_MUL_S(input_scale, input_scale)), last_dim) -
76-
XT_MUL_S(mean, mean);
70+
71+
// Shift raw sums to zero-point-centered sums, then form the variance
72+
// numerator N * sum(v^2) - sum(v)^2 exactly in integers: doing
73+
// E[x^2] - E[x]^2 in fp32 cancels catastrophically for 16-bit inputs.
74+
const int64_t n = static_cast<int64_t>(last_dim);
75+
const int64_t zp = input_zero_point;
76+
const int64_t centered_sum = static_cast<int64_t>(sum) - n * zp;
77+
const int64_t centered_sq_sum =
78+
static_cast<int64_t>(sq_sum) - 2 * zp * sum + n * zp * zp;
79+
const int64_t var_numerator =
80+
n * centered_sq_sum - centered_sum * centered_sum;
81+
82+
const float n_f = static_cast<float>(n);
83+
float mean =
84+
XT_DIV_S(XT_MUL_S(input_scale, static_cast<float>(centered_sum)), n_f);
85+
float variance = XT_MUL_S(
86+
XT_DIV_S(static_cast<float>(var_numerator), XT_MUL_S(n_f, n_f)),
87+
XT_MUL_S(input_scale, input_scale));
7788
float inv_std = XT_RECIP_S(XT_SQRT_S(XT_ADD_S(variance, (float)eps)));
7889

7990
// y = (x - mean) / std * kGamma + kBeta
8091
#pragma simd
8192
for (size_t j = 0; j < last_dim; ++j) {
8293
// Since X is quantized, we dequantize it, compute fp32 result, and
83-
// quantize the result to an int8/uint8 value.
94+
// quantize the result back to T.
8495
float val = dequantize<T>(x[j], input_scale, input_zero_point);
8596
val = (val - mean) * inv_std * weight_data[j] + bias_data[j];
8697
y[j] = quantize<T>(val, output_inv_scale, output_zero_point);
@@ -147,7 +158,7 @@ void quantized_layer_norm_out(
147158

148159
ScalarType dtype = input.scalar_type();
149160
switch (dtype) {
150-
ET_FORALL_CADENCE_QUANTIZED_TYPES(typed_quantized_layer_norm)
161+
ET_FORALL_CADENCE_QUANTIZED_TYPES_WITH_INT16(typed_quantized_layer_norm)
151162
default:
152163
ET_DCHECK_MSG(
153164
false, "Unhandled dtype %s", torch::executor::toString(dtype));
@@ -185,7 +196,7 @@ void quantized_layer_norm_per_tensor_out(
185196

186197
ScalarType dtype = input.scalar_type();
187198
switch (dtype) {
188-
ET_FORALL_CADENCE_QUANTIZED_TYPES(typed_quantized_layer_norm)
199+
ET_FORALL_CADENCE_QUANTIZED_TYPES_WITH_INT16(typed_quantized_layer_norm)
189200
default:
190201
ET_DCHECK_MSG(
191202
false, "Unhandled dtype %s", torch::executor::toString(dtype));

‎backends/cadence/hifi/operators/operators.h‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,32 @@ void quantized_relu_out(
6262
const ::executorch::aten::Tensor& out_shift,
6363
::executorch::aten::Tensor& output);
6464

65+
void quantized_layer_norm_out(
66+
::executorch::runtime::KernelRuntimeContext& ctx,
67+
const ::executorch::aten::Tensor& input,
68+
const ::executorch::aten::Tensor& in_scale,
69+
const ::executorch::aten::Tensor& in_zero_point,
70+
const ::executorch::aten::IntArrayRef normalized_shape,
71+
const ::executorch::aten::Tensor& weight,
72+
const ::executorch::aten::Tensor& bias,
73+
double eps,
74+
double output_scale,
75+
int64_t output_zero_point,
76+
::executorch::aten::Tensor& out);
77+
78+
void quantized_layer_norm_per_tensor_out(
79+
::executorch::runtime::KernelRuntimeContext& ctx,
80+
const ::executorch::aten::Tensor& input,
81+
double in_scale,
82+
int64_t in_zero_point,
83+
const ::executorch::aten::IntArrayRef normalized_shape,
84+
const ::executorch::aten::Tensor& weight,
85+
const ::executorch::aten::Tensor& bias,
86+
double eps,
87+
double output_scale,
88+
int64_t output_zero_point,
89+
::executorch::aten::Tensor& out);
90+
6591
void quantized_linear_out(
6692
::executorch::runtime::KernelRuntimeContext& ctx,
6793
const ::executorch::aten::Tensor& in,

‎backends/cadence/hifi/operators/tests/BUCK‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,27 @@ jarvis_wrapper.cxx_test(
2727
)
2828

2929

30+
jarvis_wrapper.cxx_test(
31+
name = "test_op_quantized_layer_norm",
32+
srcs = [
33+
"test_op_quantized_layer_norm.cpp",
34+
],
35+
compatible_backends = ["hifi"],
36+
labels = [ci.linux(ci.mode("fbsource//arvr/mode/platform010/dev"))],
37+
platforms = CXX,
38+
remote_execution = XTENSA_TEST_REMOTE_EXECUTION_ASIC,
39+
visibility = [
40+
"fbsource//xplat/executorch/backends/cadence/...",
41+
"fbcode//executorch/backends/cadence/...",
42+
],
43+
deps = [
44+
"fbsource//xplat/executorch/kernels/test:gtest_utils",
45+
"fbsource//xplat/executorch/backends/cadence/hifi/operators:op_quantized_layer_norm",
46+
"fbsource//xplat/executorch/backends/cadence/runtime:et_pal",
47+
"fbsource//xplat/executorch/runtime/core/exec_aten/testing_util:tensor_util",
48+
],
49+
)
50+
3051
jarvis_wrapper.cxx_test(
3152
name = "test_op_quantize_per_tensor",
3253
srcs = [

‎backends/cadence/hifi/operators/tests/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ add_executable(
5656
test_op_permute_copy.cpp
5757
test_op_quantize_per_tensor.cpp
5858
test_op_quantized_conv2d_out.cpp
59+
test_op_quantized_layer_norm.cpp
5960
test_op_quantized_matmul_out.cpp
6061
test_op_quantized_relu_out.cpp
6162
test_op_relu.cpp
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <gtest/gtest.h>
10+
11+
#include <executorch/kernels/test/TestUtil.h>
12+
#include <executorch/runtime/core/exec_aten/exec_aten.h>
13+
#include <executorch/runtime/core/exec_aten/testing_util/tensor_factory.h>
14+
#include <executorch/runtime/core/exec_aten/testing_util/tensor_util.h>
15+
16+
#include <executorch/backends/cadence/hifi/operators/operators.h>
17+
18+
namespace impl {
19+
namespace HiFi {
20+
namespace native {
21+
namespace {
22+
23+
using ::executorch::aten::IntArrayRef;
24+
using ::executorch::aten::ScalarType;
25+
using ::executorch::aten::Tensor;
26+
using ::executorch::runtime::testing::TensorFactory;
27+
28+
// Scales are powers of two and inputs are symmetric around their mean, so
29+
// every row normalizes to exactly +/-1 (eps is negligible).
30+
constexpr double kEps = 1e-9;
31+
32+
class HiFiQuantizedLayerNormTest : public OperatorTest {};
33+
34+
TEST_F(HiFiQuantizedLayerNormTest, Int16LargeMagnitudePerTensor) {
35+
TensorFactory<ScalarType::Short> tf_short;
36+
TensorFactory<ScalarType::Float> tf_float;
37+
const std::vector<int64_t> normalized_shape{8};
38+
39+
// Per-row sum of squares is 8 * 30000^2, well past int32 range.
40+
const Tensor input = tf_short.make(
41+
{2, 8},
42+
{30000, -30000, 30000, -30000, 30000, -30000, 30000, -30000,
43+
-30000, 30000, -30000, 30000, -30000, 30000, -30000, 30000});
44+
const Tensor weight = tf_float.ones({8});
45+
const Tensor bias = tf_float.zeros({8});
46+
Tensor out = tf_short.zeros({2, 8});
47+
48+
quantized_layer_norm_per_tensor_out(
49+
context_,
50+
input,
51+
/*in_scale=*/1.0 / 1024,
52+
/*in_zero_point=*/0,
53+
IntArrayRef(normalized_shape.data(), normalized_shape.size()),
54+
weight,
55+
bias,
56+
kEps,
57+
/*output_scale=*/1.0 / 16384,
58+
/*output_zero_point=*/0,
59+
out);
60+
61+
const Tensor expected = tf_short.make(
62+
{2, 8},
63+
{16384, -16384, 16384, -16384, 16384, -16384, 16384, -16384,
64+
-16384, 16384, -16384, 16384, -16384, 16384, -16384, 16384});
65+
EXPECT_TENSOR_EQ(out, expected);
66+
}
67+
68+
TEST_F(HiFiQuantizedLayerNormTest, Int16LargeMeanSmallSpreadWithZeroPoints) {
69+
TensorFactory<ScalarType::Short> tf_short;
70+
TensorFactory<ScalarType::Float> tf_float;
71+
TensorFactory<ScalarType::Long> tf_long;
72+
const std::vector<int64_t> normalized_shape{8};
73+
74+
// Centered values are 21000 +/- 4: the mean dwarfs the spread, which breaks
75+
// an fp32 E[x^2] - E[x]^2 variance.
76+
const Tensor input = tf_short.make(
77+
{1, 8}, {20004, 19996, 20004, 19996, 20004, 19996, 20004, 19996});
78+
const Tensor in_scale = tf_float.make({1}, {1.0f / 64});
79+
const Tensor in_zero_point = tf_long.make({1}, {-1000});
80+
const Tensor weight = tf_float.full({8}, 2.0f);
81+
const Tensor bias = tf_float.full({8}, 0.5f);
82+
Tensor out = tf_short.zeros({1, 8});
83+
84+
quantized_layer_norm_out(
85+
context_,
86+
input,
87+
in_scale,
88+
in_zero_point,
89+
IntArrayRef(normalized_shape.data(), normalized_shape.size()),
90+
weight,
91+
bias,
92+
kEps,
93+
/*output_scale=*/1.0 / 4096,
94+
/*output_zero_point=*/100,
95+
out);
96+
97+
// (+1 * 2 + 0.5) * 4096 + 100 and (-1 * 2 + 0.5) * 4096 + 100.
98+
const Tensor expected = tf_short.make(
99+
{1, 8}, {10340, -6044, 10340, -6044, 10340, -6044, 10340, -6044});
100+
EXPECT_TENSOR_EQ(out, expected);
101+
}
102+
103+
TEST_F(HiFiQuantizedLayerNormTest, Int8WithZeroPoint) {
104+
TensorFactory<ScalarType::Char> tf_char;
105+
TensorFactory<ScalarType::Float> tf_float;
106+
const std::vector<int64_t> normalized_shape{4};
107+
108+
const Tensor input = tf_char.make({1, 4}, {10, 30, 10, 30});
109+
const Tensor weight = tf_float.ones({4});
110+
const Tensor bias = tf_float.zeros({4});
111+
Tensor out = tf_char.zeros({1, 4});
112+
113+
quantized_layer_norm_per_tensor_out(
114+
context_,
115+
input,
116+
/*in_scale=*/1.0 / 8,
117+
/*in_zero_point=*/20,
118+
IntArrayRef(normalized_shape.data(), normalized_shape.size()),
119+
weight,
120+
bias,
121+
kEps,
122+
/*output_scale=*/1.0 / 64,
123+
/*output_zero_point=*/0,
124+
out);
125+
126+
const Tensor expected = tf_char.make({1, 4}, {-64, 64, -64, 64});
127+
EXPECT_TENSOR_EQ(out, expected);
128+
}
129+
130+
} // namespace
131+
} // namespace native
132+
} // namespace HiFi
133+
} // namespace impl

0 commit comments

Comments
 (0)