Skip to content

Commit 25db302

Browse files
mvartani-metafacebook-github-bot
authored andcommitted
Support int16 in generic quantized_layer_norm (#24093)
Summary: The generic quantized_layer_norm kernel has the same int16 gap as the HiFi kernel fixed earlier in this stack. The generic kernel is the portable implementation used on CPU and on Xtensa cores that have no backend-specific layer norm kernel. Before this change, both overloads dispatched with ET_FORALL_CADENCE_QUANTIZED_TYPES, so int16 fell through to an ET_DCHECK_MSG that compiles out in release builds and left the output unwritten. The statistics also used int32 accumulators, which overflow for int16 because one squared value can reach 2^30. The variance was formed as E[x^2] - E[x]^2, which cancels catastrophically when the mean is large compared to the spread. This applies the same fix as the HiFi kernel: - Dispatch both overloads with ET_FORALL_CADENCE_QUANTIZED_TYPES_WITH_INT16. - Accumulate in int64 for 16-bit types; 8-bit keeps 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. The per-row mean and variance stay in double as before. - Add a gtest with the same hand-computed cases as the HiFi test. Reviewed By: DrJessop Differential Revision: D126951724
1 parent 376259c commit 25db302

4 files changed

Lines changed: 205 additions & 16 deletions

File tree

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

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <cmath>
1212
#include <cstdint>
1313
#include <cstdlib>
14+
#include <type_traits>
1415

1516
#include <executorch/backends/cadence/generic/kernels/kernels.h>
1617
#include <executorch/backends/cadence/generic/operators/cadence_type_util.h>
@@ -61,29 +62,37 @@ void quantized_layer_norm_per_tensor_(
6162
const T* x = in_data + i * last_dim;
6263
T* y = out_data + i * last_dim;
6364

64-
// compute sum and squared sum. The fp32 sum can be approximated as:
65-
// (X_1 - in_zero_point) * in_scale + (X_2 - in_zero_point) * in_scale + ...
66-
// (X_N - in_zero_point) * in_scale.
67-
int32_t sum = 0;
68-
int32_t sq_sum = last_dim * input_zero_point * input_zero_point;
65+
// 16-bit squares overflow an int32 sum after two elements.
66+
using acc_t = std::conditional_t<sizeof(T) == 1, int32_t, int64_t>;
67+
acc_t sum = 0;
68+
acc_t sq_sum = 0;
6969
for (size_t j = 0; j < last_dim; ++j) {
70-
int32_t val = x[j];
70+
const int32_t val = x[j];
7171
sum += val;
72-
sq_sum += val * val;
72+
sq_sum += static_cast<acc_t>(val) * val;
7373
}
74-
sq_sum -= (2 * sum * input_zero_point);
75-
sum -= (last_dim * input_zero_point);
7674

77-
float mean = (input_scale * sum) / last_dim;
78-
float variance =
79-
(sq_sum * input_scale * input_scale) / last_dim - mean * mean;
75+
// Shift raw sums to zero-point-centered sums, then form the variance
76+
// numerator N * sum(v^2) - sum(v)^2 exactly in integers: doing
77+
// E[x^2] - E[x]^2 in fp32 cancels catastrophically for 16-bit inputs.
78+
const int64_t n = static_cast<int64_t>(last_dim);
79+
const int64_t zp = input_zero_point;
80+
const int64_t centered_sum = static_cast<int64_t>(sum) - n * zp;
81+
const int64_t centered_sq_sum =
82+
static_cast<int64_t>(sq_sum) - 2 * zp * sum + n * zp * zp;
83+
const int64_t var_numerator =
84+
n * centered_sq_sum - centered_sum * centered_sum;
85+
86+
const double n_d = static_cast<double>(n);
87+
float mean = input_scale * centered_sum / n_d;
88+
float variance = var_numerator / (n_d * n_d) * (input_scale * input_scale);
8089
float inv_std = 1.0f / std::sqrt(variance + eps);
8190

8291
// y = (x - mean) / std * kGamma + kBeta
8392
for (size_t j = 0; j < last_dim; ++j) {
8493
// y[j] = (x[j] - mean) / std * kGamma + kBeta;
8594
// Since X is quantized, we dequantize it, compute fp32 result, and
86-
// quantize the result to an int8/uint8 value.
95+
// quantize the result back to T.
8796
float val = dequantize<T>(x[j], input_scale, input_zero_point);
8897

8998
val = (val - mean) * inv_std * weight_data[j] + bias_data[j];
@@ -149,7 +158,7 @@ Tensor& quantized_layer_norm_out(
149158

150159
ScalarType dtype = input.scalar_type();
151160
switch (dtype) {
152-
ET_FORALL_CADENCE_QUANTIZED_TYPES(typed_quantized_layer_norm)
161+
ET_FORALL_CADENCE_QUANTIZED_TYPES_WITH_INT16(typed_quantized_layer_norm)
153162
default:
154163
ET_DCHECK_MSG(
155164
false, "Unhandled dtype %s", torch::executor::toString(dtype));
@@ -188,7 +197,7 @@ Tensor& quantized_layer_norm_per_tensor_out(
188197

189198
ScalarType dtype = input.scalar_type();
190199
switch (dtype) {
191-
ET_FORALL_CADENCE_QUANTIZED_TYPES(typed_quantized_layer_norm)
200+
ET_FORALL_CADENCE_QUANTIZED_TYPES_WITH_INT16(typed_quantized_layer_norm)
192201
default:
193202
ET_DCHECK_MSG(
194203
false, "Unhandled dtype %s", torch::executor::toString(dtype));

‎backends/cadence/generic/operators/tests/BUCK‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@ load("@fbsource//xplat/on_device_ai/Assistant/Jarvis/build:cxx_wrapper.bzl", "ja
44

55
oncall("executorch")
66

7+
jarvis_wrapper.cxx_test(
8+
name = "test_op_quantized_layer_norm",
9+
srcs = [
10+
"test_op_quantized_layer_norm.cpp",
11+
],
12+
compatible_backends = ["generic"],
13+
labels = [ci.linux(ci.mode("fbsource//arvr/mode/platform010/dev"))],
14+
platforms = CXX,
15+
visibility = [
16+
"fbsource//xplat/executorch/backends/cadence/...",
17+
"fbcode//executorch/backends/cadence/...",
18+
],
19+
deps = [
20+
"fbsource//xplat/executorch/backends/cadence/generic/operators:op_quantized_layer_norm",
21+
"fbsource//xplat/executorch/kernels/test:gtest_utils",
22+
"fbsource//xplat/executorch/runtime/core/exec_aten/testing_util:tensor_util",
23+
],
24+
)
25+
726
jarvis_wrapper.cxx_test(
827
name = "test_op_sdpa_bitwise_mask_gen",
928
srcs = [

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ endif()
4848
# checks with EXPECT_TENSOR_EQ, mirroring the BUCK tests.
4949
add_executable(
5050
cadence_generic_op_tests
51-
test_op_sdpa_bitwise_mask_gen.cpp
51+
test_op_quantized_layer_norm.cpp test_op_sdpa_bitwise_mask_gen.cpp
5252
${EXECUTORCH_ROOT}/runtime/core/exec_aten/testing_util/tensor_util.cpp
5353
)
5454
target_compile_definitions(cadence_generic_op_tests PRIVATE GTEST_HAS_PTHREAD=0)
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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/generic/operators/op_quantized_layer_norm.h>
17+
18+
namespace impl {
19+
namespace generic {
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 GenericQuantizedLayerNormTest : public OperatorTest {};
33+
34+
TEST_F(GenericQuantizedLayerNormTest, 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,
43+
-30000,
44+
30000,
45+
-30000,
46+
30000,
47+
-30000,
48+
30000,
49+
-30000,
50+
-30000,
51+
30000,
52+
-30000,
53+
30000,
54+
-30000,
55+
30000,
56+
-30000,
57+
30000});
58+
const Tensor weight = tf_float.ones({8});
59+
const Tensor bias = tf_float.zeros({8});
60+
Tensor out = tf_short.zeros({2, 8});
61+
62+
quantized_layer_norm_per_tensor_out(
63+
context_,
64+
input,
65+
/*in_scale=*/1.0 / 1024,
66+
/*in_zero_point=*/0,
67+
IntArrayRef(normalized_shape.data(), normalized_shape.size()),
68+
weight,
69+
bias,
70+
kEps,
71+
/*output_scale=*/1.0 / 16384,
72+
/*output_zero_point=*/0,
73+
out);
74+
75+
const Tensor expected = tf_short.make(
76+
{2, 8},
77+
{16384,
78+
-16384,
79+
16384,
80+
-16384,
81+
16384,
82+
-16384,
83+
16384,
84+
-16384,
85+
-16384,
86+
16384,
87+
-16384,
88+
16384,
89+
-16384,
90+
16384,
91+
-16384,
92+
16384});
93+
EXPECT_TENSOR_EQ(out, expected);
94+
}
95+
96+
TEST_F(GenericQuantizedLayerNormTest, Int16LargeMeanSmallSpreadWithZeroPoints) {
97+
TensorFactory<ScalarType::Short> tf_short;
98+
TensorFactory<ScalarType::Float> tf_float;
99+
TensorFactory<ScalarType::Long> tf_long;
100+
const std::vector<int64_t> normalized_shape{8};
101+
102+
// Centered values are 21000 +/- 4: the mean dwarfs the spread, which breaks
103+
// an fp32 E[x^2] - E[x]^2 variance.
104+
const Tensor input = tf_short.make(
105+
{1, 8}, {20004, 19996, 20004, 19996, 20004, 19996, 20004, 19996});
106+
const Tensor in_scale = tf_float.make({1}, {1.0f / 64});
107+
const Tensor in_zero_point = tf_long.make({1}, {-1000});
108+
const Tensor weight = tf_float.full({8}, 2.0f);
109+
const Tensor bias = tf_float.full({8}, 0.5f);
110+
Tensor out = tf_short.zeros({1, 8});
111+
112+
quantized_layer_norm_out(
113+
context_,
114+
input,
115+
in_scale,
116+
in_zero_point,
117+
IntArrayRef(normalized_shape.data(), normalized_shape.size()),
118+
weight,
119+
bias,
120+
kEps,
121+
/*output_scale=*/1.0 / 4096,
122+
/*output_zero_point=*/100,
123+
out);
124+
125+
// (+1 * 2 + 0.5) * 4096 + 100 and (-1 * 2 + 0.5) * 4096 + 100.
126+
const Tensor expected = tf_short.make(
127+
{1, 8}, {10340, -6044, 10340, -6044, 10340, -6044, 10340, -6044});
128+
EXPECT_TENSOR_EQ(out, expected);
129+
}
130+
131+
TEST_F(GenericQuantizedLayerNormTest, Int8WithZeroPoint) {
132+
TensorFactory<ScalarType::Char> tf_char;
133+
TensorFactory<ScalarType::Float> tf_float;
134+
const std::vector<int64_t> normalized_shape{4};
135+
136+
const Tensor input = tf_char.make({1, 4}, {10, 30, 10, 30});
137+
const Tensor weight = tf_float.ones({4});
138+
const Tensor bias = tf_float.zeros({4});
139+
Tensor out = tf_char.zeros({1, 4});
140+
141+
quantized_layer_norm_per_tensor_out(
142+
context_,
143+
input,
144+
/*in_scale=*/1.0 / 8,
145+
/*in_zero_point=*/20,
146+
IntArrayRef(normalized_shape.data(), normalized_shape.size()),
147+
weight,
148+
bias,
149+
kEps,
150+
/*output_scale=*/1.0 / 64,
151+
/*output_zero_point=*/0,
152+
out);
153+
154+
const Tensor expected = tf_char.make({1, 4}, {-64, 64, -64, 64});
155+
EXPECT_TENSOR_EQ(out, expected);
156+
}
157+
158+
} // namespace
159+
} // namespace native
160+
} // namespace generic
161+
} // namespace impl

0 commit comments

Comments
 (0)