Skip to content

Commit e2323a0

Browse files
authored
Vectorize min + max and add fused minmax (pytorch#22759) (pytorch#22759)
Summary: Replace the iterator-returning `std::min_element` / `std::max_element` implementations of `torch::executor::vec_minf` and `vec_maxf` with four independent value-reduction lanes. A shared compile-time implementation computes only the requested extrema, with a narrowly scoped Clang vectorization hint for NEON/SSE2 targets and no fast-math requirement. Add `torch::executor::vec_minmaxf(const float* x, size_t size, float* min_out, float* max_out)` to compute both extrema together. Wire the per-tensor and both serial/parallel per-token `choose_qparams` paths to the fused helper, eliminating their separate minimum and maximum scans without changing scale/zero-point calculations. Differential Revision: D119391738
1 parent aeb4224 commit e2323a0

4 files changed

Lines changed: 212 additions & 20 deletions

File tree

‎kernels/portable/cpu/test/vec_ops_test.cpp‎

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
#include <executorch/kernels/portable/cpu/vec_ops.h>
1010

11+
#include <algorithm>
12+
#include <cmath>
1113
#include <limits>
1214
#include <vector>
1315

@@ -16,23 +18,119 @@
1618
using namespace ::testing;
1719

1820
TEST(VecMinfTest, Smoke) {
19-
// No need to be super thorough since we know this is implemented with
20-
// std::min_element(). Just show that it's hooked up correctly.
21-
2221
constexpr size_t kNumVals = 5;
2322
float x[kNumVals] = {1.1, -2.2, 0, -1234.5, 10.0};
2423
EXPECT_EQ(torch::executor::vec_minf(x, kNumVals), -1234.5);
2524
}
2625

2726
TEST(VecMaxfTest, Smoke) {
28-
// No need to be super thorough since we know this is implemented with
29-
// std::max_element(). Just show that it's hooked up correctly.
30-
3127
constexpr size_t kNumVals = 5;
3228
float x[kNumVals] = {1.1, -2.2, 0, -1234.5, 10.0};
3329
EXPECT_EQ(torch::executor::vec_maxf(x, kNumVals), 10.0);
3430
}
3531

32+
namespace {
33+
34+
void expect_extrema(const float* x, size_t size, float min, float max) {
35+
float fused_min = 123.0f;
36+
float fused_max = -456.0f;
37+
torch::executor::vec_minmaxf(x, size, &fused_min, &fused_max);
38+
for (float actual : {torch::executor::vec_minf(x, size), fused_min}) {
39+
if (std::isnan(min)) {
40+
EXPECT_TRUE(std::isnan(actual));
41+
} else {
42+
EXPECT_EQ(actual, min);
43+
EXPECT_EQ(std::signbit(actual), std::signbit(min));
44+
}
45+
}
46+
for (float actual : {torch::executor::vec_maxf(x, size), fused_max}) {
47+
if (std::isnan(max)) {
48+
EXPECT_TRUE(std::isnan(actual));
49+
} else {
50+
EXPECT_EQ(actual, max);
51+
EXPECT_EQ(std::signbit(actual), std::signbit(max));
52+
}
53+
}
54+
}
55+
56+
class VecMinMaxfTest : public TestWithParam<size_t> {};
57+
58+
TEST_P(VecMinMaxfTest, EveryExtremumPositionAndUnalignedInput) {
59+
const size_t size = GetParam();
60+
std::vector<float> storage(size + 1, 7.0f);
61+
float* x = storage.data() + 1;
62+
for (size_t i = 0; i < size; ++i) {
63+
SCOPED_TRACE(i);
64+
x[i] = -13.0f;
65+
expect_extrema(x, size, -13.0f, size == 1 ? -13.0f : 7.0f);
66+
x[i] = 19.0f;
67+
expect_extrema(x, size, size == 1 ? 19.0f : 7.0f, 19.0f);
68+
x[i] = 7.0f;
69+
}
70+
}
71+
72+
TEST_P(VecMinMaxfTest, LeadingNaNPersistsAndLaterNaNsAreIgnored) {
73+
const size_t size = GetParam();
74+
const float nan = std::numeric_limits<float>::quiet_NaN();
75+
std::vector<float> x(size, 3.0f);
76+
for (size_t i = 0; i < size; ++i) {
77+
SCOPED_TRACE(i);
78+
x[i] = nan;
79+
expect_extrema(x.data(), size, i == 0 ? nan : 3.0f, i == 0 ? nan : 3.0f);
80+
x[i] = 3.0f;
81+
}
82+
std::fill(x.begin(), x.end(), nan);
83+
expect_extrema(x.data(), size, nan, nan);
84+
}
85+
86+
TEST_P(VecMinMaxfTest, Infinities) {
87+
const size_t size = GetParam();
88+
const float inf = std::numeric_limits<float>::infinity();
89+
std::vector<float> x(size, inf);
90+
expect_extrema(x.data(), size, inf, inf);
91+
std::fill(x.begin(), x.end(), -inf);
92+
expect_extrema(x.data(), size, -inf, -inf);
93+
if (size > 1) {
94+
x.back() = inf;
95+
expect_extrema(x.data(), size, -inf, inf);
96+
}
97+
}
98+
99+
TEST_P(VecMinMaxfTest, FirstSignedZeroWins) {
100+
const size_t size = GetParam();
101+
for (float first_zero : {0.0f, -0.0f}) {
102+
for (size_t first = 0; first < size; ++first) {
103+
SCOPED_TRACE(first);
104+
for (float other : {-1.0f, 1.0f}) {
105+
std::vector<float> x(size, other);
106+
x[first] = first_zero;
107+
for (size_t j = first + 1; j < size; ++j) {
108+
x[j] = -first_zero;
109+
}
110+
expect_extrema(
111+
x.data(),
112+
size,
113+
first == 0 || other > 0 ? first_zero : other,
114+
first == 0 || other < 0 ? first_zero : other);
115+
}
116+
}
117+
}
118+
}
119+
120+
INSTANTIATE_TEST_SUITE_P(
121+
VectorBoundaries,
122+
VecMinMaxfTest,
123+
Values(1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 127, 128, 129));
124+
125+
TEST(VecMinMaxfTest, OutputsMayAliasInput) {
126+
float x[] = {4.0f, -9.0f, 2.0f, 11.0f, -3.0f};
127+
torch::executor::vec_minmaxf(x, 5, &x[0], &x[1]);
128+
EXPECT_EQ(x[0], -9.0f);
129+
EXPECT_EQ(x[1], 11.0f);
130+
}
131+
132+
} // namespace
133+
36134
TEST(VecAddfTest, Smoke) {
37135
constexpr size_t kNumVals = 5;
38136
float in1[kNumVals] = {1, 2, 3, 4, 5};

‎kernels/portable/cpu/vec_ops.h‎

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,104 @@
2929
namespace torch {
3030
namespace executor {
3131

32-
/// Returns the minimum element of the array at `x`, which must have `size`
33-
/// elements.
32+
namespace internal {
33+
34+
template <bool ComputeMin, bool ComputeMax>
35+
inline void
36+
vec_minmaxf_impl(const float* x, size_t size, float* min_out, float* max_out) {
37+
float min_value = x[0];
38+
float max_value = x[0];
39+
size_t i = 1;
40+
if (size >= 4) {
41+
// Seeding every lane with x[0] preserves a leading NaN and ignores later
42+
// NaNs.
43+
ET_UNUSED float mins[4] = {x[0], x[0], x[0], x[0]};
44+
ET_UNUSED float maxs[4] = {x[0], x[0], x[0], x[0]};
45+
const size_t vector_end = size - size % 4;
46+
for (i = 0; i < vector_end; i += 4) {
47+
// Keep the independent lanes together even under size optimization.
48+
#if defined(__clang__) && (defined(__ARM_NEON) || defined(__SSE2__))
49+
#pragma clang loop vectorize_width(4) interleave_count(1) unroll(disable)
50+
#endif
51+
for (size_t j = 0; j < 4; ++j) {
52+
if constexpr (ComputeMin) {
53+
mins[j] = std::min(mins[j], x[i + j]);
54+
}
55+
if constexpr (ComputeMax) {
56+
maxs[j] = std::max(maxs[j], x[i + j]);
57+
}
58+
}
59+
}
60+
if constexpr (ComputeMin) {
61+
min_value =
62+
std::min(std::min(mins[0], mins[1]), std::min(mins[2], mins[3]));
63+
}
64+
if constexpr (ComputeMax) {
65+
max_value =
66+
std::max(std::max(maxs[0], maxs[1]), std::max(maxs[2], maxs[3]));
67+
}
68+
}
69+
for (; i < size; ++i) {
70+
if constexpr (ComputeMin) {
71+
min_value = std::min(min_value, x[i]);
72+
}
73+
if constexpr (ComputeMax) {
74+
max_value = std::max(max_value, x[i]);
75+
}
76+
}
77+
78+
// Lane reduction can reorder equal signed zeros. Preserve the first zero.
79+
if (size >= 4 &&
80+
((ComputeMin && min_value == 0.0f) ||
81+
(ComputeMax && max_value == 0.0f))) {
82+
for (size_t j = 0; j < size; ++j) {
83+
if (x[j] == 0.0f) {
84+
if constexpr (ComputeMin) {
85+
if (min_value == 0.0f) {
86+
min_value = x[j];
87+
}
88+
}
89+
if constexpr (ComputeMax) {
90+
if (max_value == 0.0f) {
91+
max_value = x[j];
92+
}
93+
}
94+
break;
95+
}
96+
}
97+
}
98+
if constexpr (ComputeMin) {
99+
*min_out = min_value;
100+
}
101+
if constexpr (ComputeMax) {
102+
*max_out = max_value;
103+
}
104+
}
105+
106+
} // namespace internal
107+
108+
/// Returns the minimum element of the nonempty array at `x`, which must have
109+
/// `size` elements.
34110
inline float vec_minf(const float* x, size_t size) {
35-
return *std::min_element(x, x + size);
111+
float minimum;
112+
internal::vec_minmaxf_impl<true, false>(x, size, &minimum, nullptr);
113+
return minimum;
36114
}
37115

38-
/// Returns the maximum element of the array at `x`, which must have `size`
39-
/// elements.
116+
/// Returns the maximum element of the nonempty array at `x`, which must have
117+
/// `size` elements.
40118
inline float vec_maxf(const float* x, size_t size) {
41-
return *std::max_element(x, x + size);
119+
float maximum;
120+
internal::vec_minmaxf_impl<false, true>(x, size, nullptr, &maximum);
121+
return maximum;
122+
}
123+
124+
/// Writes the minimum and maximum of the nonempty array at `x`, which must have
125+
/// `size` elements. `min_out` and `max_out` must point to distinct valid
126+
/// floats.
127+
inline void
128+
vec_minmaxf(const float* x, size_t size, float* min_out, float* max_out) {
129+
internal::vec_minmaxf_impl<true, true>(x, size, min_out, max_out);
42130
}
43131

44132
/// Add each element of `x` and `y` into the corresponding element of `z`. All

‎kernels/quantized/cpu/op_choose_qparams.cpp‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,9 @@ void choose_qparams(
179179
Tensor& zero_point_out) {
180180
const float* x_fp32 = input.const_data_ptr<float>();
181181
// Compute x_min, x_max and q_params (scale, zero_point)
182-
float min = torch::executor::vec_minf(x_fp32, input.numel());
183-
float max = torch::executor::vec_maxf(x_fp32, input.numel());
182+
float min;
183+
float max;
184+
torch::executor::vec_minmaxf(x_fp32, input.numel(), &min, &max);
184185

185186
double scale;
186187
int32_t zero_point;
@@ -216,8 +217,10 @@ void choose_qparams_per_token(
216217
0, num_tokens, 1, [&](const int64_t begin, const int64_t end) {
217218
for (int64_t i = begin; i < end; i++) {
218219
const float* token_data = x_fp32 + i * token_dim_size;
219-
float min = torch::executor::vec_minf(token_data, token_dim_size);
220-
float max = torch::executor::vec_maxf(token_data, token_dim_size);
220+
float min;
221+
float max;
222+
torch::executor::vec_minmaxf(
223+
token_data, token_dim_size, &min, &max);
221224
double scale;
222225
int32_t zero_point;
223226
calculate_scale_and_zero_point(
@@ -228,10 +231,9 @@ void choose_qparams_per_token(
228231
});
229232
} else {
230233
for (auto i = 0; i < num_tokens; i++) {
231-
// vec_minf uses std::min_element. Check if it actually
232-
// gets vectorized.
233-
float min = torch::executor::vec_minf(x_fp32, token_dim_size);
234-
float max = torch::executor::vec_maxf(x_fp32, token_dim_size);
234+
float min;
235+
float max;
236+
torch::executor::vec_minmaxf(x_fp32, token_dim_size, &min, &max);
235237
double scale;
236238
int32_t zero_point;
237239
calculate_scale_and_zero_point(min, max, qmin, qmax, scale, zero_point);

‎kernels/quantized/test/op_choose_qparams_test.cpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ void test_dtype() {
5050
EXPECT_TENSOR_EQ(zero_point_out, expected_zero_point);
5151
}
5252

53+
TEST(OpChooseQparamsTensorOutTest, Byte) {
54+
test_dtype<ScalarType::Byte>();
55+
}
56+
5357
TEST(OpChooseQparamsPerTokenAsymmetricTensorOutTest, Float) {
5458
et_pal_init();
5559
TensorFactory<ScalarType::Float> tf_float;

0 commit comments

Comments
 (0)