Skip to content

Commit 109a99f

Browse files
authored
Add channels-last support to portable mean (pytorch#23275)
### Summary Fixes pytorch#19284. Portable mean rejects channels-last inputs, including spatial reductions that produce contiguous outputs. Reuse the stride-aware reduction helper and map logical output indices through the output strides to support 4D and 5D channels-last tensors and mixed input/output layouts. Preserve the contiguous fast path when both tensors use the default dimension order. Add coverage for reduction dimensions, keepdim, dtype conversion, BFloat16 accumulation, empty inputs, and dynamic outputs. ### Test plan ```bash cmake --build build --target portable_kernels_test optimized_portable_kernels_test -j 8 build/kernels/test/portable_kernels_test --gtest_filter='OpMeanOutTest.*' build/kernels/test/optimized_portable_kernels_test --gtest_filter='OpMeanOutTest.*' lintrunner kernels/portable/cpu/op_mean.cpp kernels/test/op_mean_test.cpp git diff --check ``` All 24 enabled mean tests pass in each build, including eight new regressions. Confirmed ChannelsLastSpatialReduction fails with the original kernel and passes with this change. Lint and whitespace checks pass. Authored with OpenAI Codex.
1 parent bdb489e commit 109a99f

2 files changed

Lines changed: 174 additions & 7 deletions

File tree

‎kernels/portable/cpu/op_mean.cpp‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,20 @@ Tensor& mean_dim_out(
3636
InvalidArgument,
3737
out);
3838

39-
ET_KERNEL_CHECK(
40-
ctx, tensors_have_same_dim_order(in, out), InvalidArgument, out);
41-
42-
ET_KERNEL_CHECK(ctx, tensor_is_default_dim_order(in), InvalidArgument, out);
43-
4439
ET_KERNEL_CHECK(
4540
ctx,
4641
resize_reduction_out(in, dim_list, keepdim, out) == Error::Ok,
4742
InvalidArgument,
4843
out);
4944

45+
const bool out_is_contiguous =
46+
is_contiguous_dim_order(out.dim_order().data(), out.dim());
47+
5048
// Fast path: contiguous tensor, single innermost dim reduction, same dtype.
5149
// Bypasses generic MapReduceOverDimListPlan to use a tight vectorizable loop.
5250
if (in.numel() > 0 && dim_list.has_value() && dim_list.value().size() == 1 &&
53-
in.scalar_type() == out.scalar_type()) {
51+
in.scalar_type() == out.scalar_type() && out_is_contiguous &&
52+
is_contiguous_dim_order(in.dim_order().data(), in.dim())) {
5453
const int64_t d = dim_list.value()[0] < 0 ? dim_list.value()[0] + in.dim()
5554
: dim_list.value()[0];
5655
if (d >= 0 && d < in.dim() && d == in.dim() - 1 &&
@@ -109,7 +108,17 @@ Tensor& mean_dim_out(
109108
[](ACC outv, ACC acc) { return acc + outv; },
110109
out_ix);
111110
}
112-
out_data[out_ix] =
111+
// The reduction plan indexes outputs in logical dimension order.
112+
size_t out_offset = out_ix;
113+
if (!out_is_contiguous) {
114+
out_offset = 0;
115+
size_t ix = out_ix;
116+
for (int64_t d = out.dim() - 1; d >= 0; --d) {
117+
out_offset += (ix % out.size(d)) * out.strides()[d];
118+
ix /= out.size(d);
119+
}
120+
}
121+
out_data[out_offset] =
113122
static_cast<CTYPE_OUT>(sum / static_cast<float>(num));
114123
}
115124
});

‎kernels/test/op_mean_test.cpp‎

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
#include <executorch/test/utils/DeathTest.h>
1818
#include <gtest/gtest.h>
1919
#include <cmath>
20+
#include <numeric>
21+
#include <vector>
2022

2123
using namespace ::testing;
2224
using executorch::aten::ArrayRef;
@@ -291,6 +293,162 @@ TEST_F(OpMeanOutTest, BFloat16LargeDimAccumulatesInFloat) {
291293
EXPECT_TENSOR_CLOSE(out, expected);
292294
}
293295

296+
TEST_F(OpMeanOutTest, ChannelsLastSpatialReduction) {
297+
TensorFactory<ScalarType::Float> tf;
298+
std::vector<float> data(120);
299+
std::iota(data.begin(), data.end(), 0.0f);
300+
Tensor x = tf.channels_last_like(tf.make({2, 3, 4, 5}, data));
301+
Tensor out = tf.zeros({2, 3});
302+
const int64_t dims[] = {2, 3};
303+
304+
op_mean_out(x, ArrayRef<int64_t>(dims), false, {}, out);
305+
306+
EXPECT_TENSOR_CLOSE(
307+
out, tf.make({2, 3}, {9.5, 29.5, 49.5, 69.5, 89.5, 109.5}));
308+
}
309+
310+
TEST_F(OpMeanOutTest, ChannelsLastReductionDimensions) {
311+
TensorFactory<ScalarType::Float> tf;
312+
std::vector<float> data(120);
313+
std::iota(data.begin(), data.end(), 0.0f);
314+
Tensor contiguous = tf.make({2, 3, 4, 5}, data);
315+
Tensor channels_last = tf.channels_last_like(contiguous);
316+
317+
for (int mask = 0; mask < 16; ++mask) {
318+
SCOPED_TRACE(mask);
319+
std::vector<int64_t> dims;
320+
for (int d = 3; d >= 0; --d) {
321+
if (mask & (1 << d)) {
322+
dims.push_back(d - 4);
323+
}
324+
}
325+
const ArrayRef<int64_t> dim_list(dims.data(), dims.size());
326+
for (bool keepdim : {false, true}) {
327+
SCOPED_TRACE(keepdim);
328+
std::vector<int32_t> sizes;
329+
for (int d = 0; d < 4; ++d) {
330+
if (mask == 0 || (mask & (1 << d))) {
331+
if (keepdim) {
332+
sizes.push_back(1);
333+
}
334+
} else {
335+
sizes.push_back(contiguous.size(d));
336+
}
337+
}
338+
Tensor expected = tf.zeros(sizes);
339+
op_mean_out(contiguous, dim_list, keepdim, {}, expected);
340+
341+
Tensor out = tf.zeros(sizes);
342+
op_mean_out(channels_last, dim_list, keepdim, {}, out);
343+
EXPECT_TENSOR_CLOSE(out, expected);
344+
345+
if (keepdim) {
346+
Tensor expected_channels_last = tf.channels_last_like(expected);
347+
for (const Tensor& input : {contiguous, channels_last}) {
348+
Tensor out_channels_last = tf.zeros_channels_last(sizes);
349+
op_mean_out(input, dim_list, keepdim, {}, out_channels_last);
350+
EXPECT_TENSOR_CLOSE(out_channels_last, expected_channels_last);
351+
}
352+
}
353+
}
354+
}
355+
}
356+
357+
TEST_F(OpMeanOutTest, ChannelsLastAllDimensions) {
358+
TensorFactory<ScalarType::Float> tf;
359+
Tensor x =
360+
tf.channels_last_like(tf.make({1, 2, 2, 2}, {0, 1, 2, 3, 4, 5, 6, 7}));
361+
Tensor out = tf.zeros({});
362+
op_mean_out(x, {}, false, {}, out);
363+
EXPECT_TENSOR_CLOSE(out, tf.make({}, {3.5}));
364+
365+
op_mean_dtype_out(x, {}, out);
366+
EXPECT_TENSOR_CLOSE(out, tf.make({}, {3.5}));
367+
368+
Tensor out_keepdim = tf.zeros_channels_last({1, 1, 1, 1});
369+
op_mean_out(x, {}, true, {}, out_keepdim);
370+
EXPECT_TENSOR_CLOSE(out_keepdim, tf.make_channels_last({1, 1, 1, 1}, {3.5}));
371+
}
372+
373+
TEST_F(OpMeanOutTest, ChannelsLast3d) {
374+
TensorFactory<ScalarType::Float> tf;
375+
std::vector<float> data(48);
376+
std::iota(data.begin(), data.end(), 0.0f);
377+
Tensor x = tf.make_with_dimorder({2, 3, 2, 2, 2}, data, {0, 2, 3, 4, 1});
378+
Tensor out = tf.zeros({2, 3});
379+
const int64_t dims[] = {2, 3, 4};
380+
op_mean_out(x, ArrayRef<int64_t>(dims), false, {}, out);
381+
EXPECT_TENSOR_CLOSE(
382+
out, tf.make({2, 3}, {10.5, 11.5, 12.5, 34.5, 35.5, 36.5}));
383+
384+
Tensor out_keepdim = tf.make_with_dimorder(
385+
{1, 3, 2, 2, 2}, std::vector<float>(24), {0, 2, 3, 4, 1});
386+
op_mean_out(x, ArrayRef<int64_t>{0}, true, {}, out_keepdim);
387+
std::vector<float> expected_data(24);
388+
std::iota(expected_data.begin(), expected_data.end(), 12.0f);
389+
EXPECT_TENSOR_CLOSE(
390+
out_keepdim,
391+
tf.make_with_dimorder({1, 3, 2, 2, 2}, expected_data, {0, 2, 3, 4, 1}));
392+
393+
Tensor out_4d = tf.zeros_channels_last({2, 3, 2, 2});
394+
op_mean_out(x, ArrayRef<int64_t>{-1}, false, {}, out_4d);
395+
EXPECT_TENSOR_CLOSE(
396+
out_4d,
397+
tf.make_channels_last(
398+
{2, 3, 2, 2}, {1.5, 2.5, 3.5, 7.5, 8.5, 9.5, 13.5, 14.5,
399+
15.5, 19.5, 20.5, 21.5, 25.5, 26.5, 27.5, 31.5,
400+
32.5, 33.5, 37.5, 38.5, 39.5, 43.5, 44.5, 45.5}));
401+
}
402+
403+
TEST_F(OpMeanOutTest, ChannelsLastDTypeConversion) {
404+
TensorFactory<ScalarType::Int> tf_in;
405+
TensorFactory<ScalarType::Float> tf_out;
406+
Tensor x = tf_in.channels_last_like(
407+
tf_in.make({1, 2, 2, 2}, {0, 1, 2, 3, 4, 5, 6, 7}));
408+
Tensor out = tf_out.zeros_channels_last({1, 2, 2, 1});
409+
op_mean_out(x, ArrayRef<int64_t>{3}, true, ScalarType::Float, out);
410+
EXPECT_TENSOR_CLOSE(
411+
out,
412+
tf_out.channels_last_like(
413+
tf_out.make({1, 2, 2, 1}, {0.5, 2.5, 4.5, 6.5})));
414+
}
415+
416+
TEST_F(OpMeanOutTest, ChannelsLastBFloat16AccumulatesInFloat) {
417+
TensorFactory<ScalarType::BFloat16> tf;
418+
Tensor x = tf.full_channels_last({1, 2, 16, 32}, 1);
419+
Tensor out = tf.zeros({1, 2});
420+
const int64_t dims[] = {2, 3};
421+
op_mean_out(x, ArrayRef<int64_t>(dims), false, {}, out);
422+
EXPECT_TENSOR_CLOSE(out, tf.ones({1, 2}));
423+
}
424+
425+
TEST_F(OpMeanOutTest, ChannelsLastEmptyInput) {
426+
TensorFactory<ScalarType::Float> tf;
427+
Tensor x = tf.make_channels_last({2, 3, 0, 4}, {});
428+
Tensor out = tf.zeros_channels_last({2, 3, 1, 4});
429+
op_mean_out(x, ArrayRef<int64_t>{2}, true, {}, out);
430+
EXPECT_TENSOR_CLOSE(out, tf.full_channels_last({2, 3, 1, 4}, NAN));
431+
432+
Tensor empty_out = tf.make_channels_last({2, 3, 0, 1}, {});
433+
op_mean_out(x, ArrayRef<int64_t>{3}, true, {}, empty_out);
434+
EXPECT_TENSOR_CLOSE(empty_out, tf.make_channels_last({2, 3, 0, 1}, {}));
435+
436+
Tensor scalar_out = tf.zeros({});
437+
op_mean_dtype_out(x, {}, scalar_out);
438+
EXPECT_TENSOR_CLOSE(scalar_out, tf.make({}, {NAN}));
439+
}
440+
441+
TEST_F(OpMeanOutTest, ChannelsLastDynamicOutput) {
442+
TensorFactory<ScalarType::Float> tf;
443+
Tensor x =
444+
tf.channels_last_like(tf.make({1, 2, 2, 2}, {0, 1, 2, 3, 4, 5, 6, 7}));
445+
Tensor out = tf.zeros_channels_last(
446+
{2, 3, 4, 5}, torch::executor::TensorShapeDynamism::DYNAMIC_BOUND);
447+
op_mean_out(x, ArrayRef<int64_t>{3}, true, {}, out);
448+
EXPECT_TENSOR_CLOSE(
449+
out, tf.channels_last_like(tf.make({1, 2, 2, 1}, {0.5, 2.5, 4.5, 6.5})));
450+
}
451+
294452
TEST_F(OpMeanOutTest, InvalidDimensionListDies) {
295453
ET_SKIP_IF(
296454
torch::executor::testing::SupportedFeatures::get()->is_aten,

0 commit comments

Comments
 (0)