Skip to content

Commit 14e38b9

Browse files
mvartani-metafacebook-github-bot
authored andcommitted
Support int16 in generic quantized_layer_norm
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 64c1675 commit 14e38b9

4 files changed

Lines changed: 177 additions & 14 deletions

File tree

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

Lines changed: 24 additions & 14 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,38 @@ 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;
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;
7888
float variance =
79-
(sq_sum * input_scale * input_scale) / last_dim - mean * mean;
89+
var_numerator / (n_d * n_d) * (input_scale * input_scale);
8090
float inv_std = 1.0f / std::sqrt(variance + eps);
8191

8292
// y = (x - mean) / std * kGamma + kBeta
8393
for (size_t j = 0; j < last_dim; ++j) {
8494
// y[j] = (x[j] - mean) / std * kGamma + kBeta;
8595
// Since X is quantized, we dequantize it, compute fp32 result, and
86-
// quantize the result to an int8/uint8 value.
96+
// quantize the result back to T.
8797
float val = dequantize<T>(x[j], input_scale, input_zero_point);
8898

8999
val = (val - mean) * inv_std * weight_data[j] + bias_data[j];
@@ -149,7 +159,7 @@ Tensor& quantized_layer_norm_out(
149159

150160
ScalarType dtype = input.scalar_type();
151161
switch (dtype) {
152-
ET_FORALL_CADENCE_QUANTIZED_TYPES(typed_quantized_layer_norm)
162+
ET_FORALL_CADENCE_QUANTIZED_TYPES_WITH_INT16(typed_quantized_layer_norm)
153163
default:
154164
ET_DCHECK_MSG(
155165
false, "Unhandled dtype %s", torch::executor::toString(dtype));
@@ -188,7 +198,7 @@ Tensor& quantized_layer_norm_per_tensor_out(
188198

189199
ScalarType dtype = input.scalar_type();
190200
switch (dtype) {
191-
ET_FORALL_CADENCE_QUANTIZED_TYPES(typed_quantized_layer_norm)
201+
ET_FORALL_CADENCE_QUANTIZED_TYPES_WITH_INT16(typed_quantized_layer_norm)
192202
default:
193203
ET_DCHECK_MSG(
194204
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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ endif()
4848
# checks with EXPECT_TENSOR_EQ, mirroring the BUCK tests.
4949
add_executable(
5050
cadence_generic_op_tests
51+
test_op_quantized_layer_norm.cpp
5152
test_op_sdpa_bitwise_mask_gen.cpp
5253
${EXECUTORCH_ROOT}/runtime/core/exec_aten/testing_util/tensor_util.cpp
5354
)
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/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, -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(GenericQuantizedLayerNormTest, 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(GenericQuantizedLayerNormTest, 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 generic
133+
} // namespace impl

0 commit comments

Comments
 (0)