Skip to content

Commit c8e3cbc

Browse files
Optimize scale-factor-2 nearest-neighbor upsampling portable op (#22999)
Add portable NCHW and AArch64 FP16 NEON fast paths. Preserve the portable kernel as the fallback for unsupported scales, dtypes, and layouts. Across five 20-iteration repeats, improved YOLO26 FP16 640x640 median latency by 2.9%. Delegation and binary size unchanged. cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @Sebastian-Larsson @robell @rascani Signed-off-by: Måns Nilsson <mans.nilsson@arm.com> Co-authored-by: Jacob Stevens <stevens.jacob1492@gmail.com>
1 parent e409469 commit c8e3cbc

13 files changed

Lines changed: 375 additions & 1 deletion

‎kernels/optimized/CMakeLists.txt‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (c) Meta Platforms, Inc. and affiliates.
22
# All rights reserved.
3+
# Copyright 2026 Arm Limited and/or its affiliates.
34
#
45
# This source code is licensed under the BSD-style license found in the
56
# LICENSE file in the root directory of this source tree.
@@ -107,6 +108,25 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64" OR ANDROID_ABI STREQUAL
107108
)
108109
endif()
109110

111+
# The nearest-2x fast path uses only baseline AArch64 Advanced SIMD. Keep
112+
# arm_neon.h and its implementation out of all other architecture builds.
113+
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64" OR ANDROID_ABI STREQUAL
114+
"arm64-v8a"
115+
)
116+
add_library(
117+
upsample_nearest2d_neon_impl OBJECT
118+
${EXECUTORCH_ROOT}/kernels/optimized/cpu/op_upsample_nearest2d_neon.cpp
119+
)
120+
target_compile_options(
121+
upsample_nearest2d_neon_impl PRIVATE ${_common_compile_options}
122+
)
123+
target_link_libraries(upsample_nearest2d_neon_impl PRIVATE executorch_core)
124+
target_link_libraries(
125+
optimized_kernels
126+
PRIVATE $<BUILD_LOCAL_INTERFACE:upsample_nearest2d_neon_impl>
127+
)
128+
endif()
129+
110130
# Build a library for _optimized_kernels_srcs
111131
#
112132
# optimized_ops_lib: Register optimized ops kernels into Executorch runtime
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2026 Arm Limited and/or its affiliates.
3+
*
4+
* This source code is licensed under the BSD-style license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <executorch/kernels/portable/cpu/util/upsample_util.h>
9+
#include <executorch/runtime/kernel/kernel_includes.h>
10+
#include <executorch/runtime/kernel/thread_parallel_interface.h>
11+
12+
#include <algorithm>
13+
14+
#if defined(__aarch64__)
15+
#include <executorch/kernels/optimized/cpu/op_upsample_nearest2d_neon.h>
16+
#endif
17+
18+
namespace torch {
19+
namespace executor {
20+
namespace native {
21+
22+
using executorch::aten::ScalarType;
23+
using executorch::aten::Tensor;
24+
25+
Tensor& upsample_nearest2d_vec_out(
26+
KernelRuntimeContext& ctx,
27+
const Tensor& in,
28+
executorch::aten::OptionalArrayRef<int64_t> output_size,
29+
executorch::aten::OptionalArrayRef<double> scale_factors,
30+
Tensor& out);
31+
32+
Tensor& opt_upsample_nearest2d_vec_out(
33+
KernelRuntimeContext& ctx,
34+
const Tensor& in,
35+
const executorch::aten::OptionalArrayRef<int64_t> output_size,
36+
const executorch::aten::OptionalArrayRef<double> scale_factors,
37+
Tensor& out) {
38+
#if !defined(__aarch64__)
39+
return upsample_nearest2d_vec_out(ctx, in, output_size, scale_factors, out);
40+
#else
41+
if (in.scalar_type() != ScalarType::Half ||
42+
!tensor_is_default_dim_order(in) || !tensor_is_default_dim_order(out) ||
43+
!tensor_is_contiguous(in) || !tensor_is_contiguous(out) ||
44+
!check_upsample_nearest2d_args(in, output_size, scale_factors, out)) {
45+
return upsample_nearest2d_vec_out(ctx, in, output_size, scale_factors, out);
46+
}
47+
48+
double scale_h;
49+
double scale_w;
50+
ET_KERNEL_CHECK_MSG(
51+
ctx,
52+
resize_upsample_2d(
53+
in, output_size, scale_factors, scale_h, scale_w, out) == Error::Ok,
54+
InvalidArgument,
55+
out,
56+
"Failed to resize output tensor");
57+
58+
const auto kernel_scale_h =
59+
area_pixel_compute_scale<double>(in.size(2), out.size(2), false, scale_h);
60+
const auto kernel_scale_w =
61+
area_pixel_compute_scale<double>(in.size(3), out.size(3), false, scale_w);
62+
if (!tensor_is_contiguous(out) || kernel_scale_h != 0.5 ||
63+
kernel_scale_w != 0.5 || out.size(2) != in.size(2) * 2 ||
64+
out.size(3) != in.size(3) * 2) {
65+
return upsample_nearest2d_vec_out(ctx, in, output_size, scale_factors, out);
66+
}
67+
68+
const int64_t output_plane_size = out.size(2) * out.size(3);
69+
const int64_t grain_size = std::max<int64_t>(
70+
1, ::executorch::extension::internal::GRAIN_SIZE / output_plane_size);
71+
const int64_t planes = in.size(0) * in.size(1);
72+
const auto* input = reinterpret_cast<const uint16_t*>(in.const_data_ptr());
73+
auto* output = reinterpret_cast<uint16_t*>(out.mutable_data_ptr());
74+
75+
const bool success = ::executorch::extension::parallel_for(
76+
0, planes, grain_size, [&](const auto begin, const auto end) {
77+
opt_upsample_nearest2d_internal::upsample_nearest2d_2x_nchw_u16(
78+
input, output, begin, end, in.size(2), in.size(3));
79+
});
80+
if (!success) {
81+
return upsample_nearest2d_vec_out(ctx, in, output_size, scale_factors, out);
82+
}
83+
return out;
84+
#endif
85+
}
86+
87+
} // namespace native
88+
} // namespace executor
89+
} // namespace torch
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2026 Arm Limited and/or its affiliates.
3+
*
4+
* This source code is licensed under the BSD-style license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#if defined(__aarch64__)
9+
10+
#include <executorch/kernels/optimized/cpu/op_upsample_nearest2d_neon.h>
11+
12+
#include <arm_neon.h>
13+
14+
#include <cstring>
15+
16+
namespace torch {
17+
namespace executor {
18+
namespace native {
19+
namespace opt_upsample_nearest2d_internal {
20+
21+
void upsample_nearest2d_2x_nchw_u16(
22+
const uint16_t* input,
23+
uint16_t* output,
24+
const int64_t begin,
25+
const int64_t end,
26+
const int64_t input_height,
27+
const int64_t input_width) {
28+
const int64_t output_height = input_height * 2;
29+
const int64_t output_width = input_width * 2;
30+
const int64_t input_plane_size = input_height * input_width;
31+
const int64_t output_plane_size = output_height * output_width;
32+
33+
for (int64_t plane = begin; plane < end; ++plane) {
34+
const uint16_t* input_plane = input + plane * input_plane_size;
35+
uint16_t* output_plane = output + plane * output_plane_size;
36+
37+
for (int64_t h = 0; h < input_height; ++h) {
38+
const uint16_t* input_row = input_plane + h * input_width;
39+
uint16_t* output_row = output_plane + (2 * h) * output_width;
40+
int64_t w = 0;
41+
for (; w + 8 <= input_width; w += 8) {
42+
const uint16x8_t values = vld1q_u16(input_row + w);
43+
uint16x8x2_t duplicated;
44+
duplicated.val[0] = values;
45+
duplicated.val[1] = values;
46+
vst2q_u16(output_row + 2 * w, duplicated);
47+
}
48+
for (; w < input_width; ++w) {
49+
const uint16_t value = input_row[w];
50+
output_row[2 * w] = value;
51+
output_row[2 * w + 1] = value;
52+
}
53+
std::memcpy(
54+
output_row + output_width,
55+
output_row,
56+
output_width * sizeof(uint16_t));
57+
}
58+
}
59+
}
60+
61+
} // namespace opt_upsample_nearest2d_internal
62+
} // namespace native
63+
} // namespace executor
64+
} // namespace torch
65+
66+
#endif
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2026 Arm Limited and/or its affiliates.
3+
*
4+
* This source code is licensed under the BSD-style license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <cstdint>
11+
12+
#if defined(__aarch64__)
13+
14+
namespace torch {
15+
namespace executor {
16+
namespace native {
17+
namespace opt_upsample_nearest2d_internal {
18+
19+
void upsample_nearest2d_2x_nchw_u16(
20+
const uint16_t* input,
21+
uint16_t* output,
22+
int64_t begin,
23+
int64_t end,
24+
int64_t input_height,
25+
int64_t input_width);
26+
27+
} // namespace opt_upsample_nearest2d_internal
28+
} // namespace native
29+
} // namespace executor
30+
} // namespace torch
31+
32+
#endif

‎kernels/optimized/cpu/targets.bzl‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,19 @@ def define_common_targets():
109109
],
110110
)
111111

112+
runtime.cxx_library(
113+
name = "upsample_nearest2d_neon_impl",
114+
srcs = select({
115+
"DEFAULT": [],
116+
"ovr_config//cpu:arm64": ["op_upsample_nearest2d_neon.cpp"],
117+
}),
118+
exported_headers = ["op_upsample_nearest2d_neon.h"],
119+
visibility = ["PUBLIC"],
120+
exported_deps = [
121+
"//executorch/runtime/kernel:kernel_includes",
122+
],
123+
)
124+
112125
# Used for dtype selective build. Collect source and header files.
113126
runtime.filegroup(
114127
name = "optimized_source_files",

‎kernels/optimized/optimized.yaml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# Copyright 2026 Arm Limited and/or its affiliates.
23
#
34
# This yaml file contains operators that have optimized kernels available.
45

@@ -121,3 +122,8 @@
121122
kernels:
122123
- arg_meta: null
123124
kernel_name: torch::executor::opt_where_out
125+
126+
- op: upsample_nearest2d.vec_out
127+
kernels:
128+
- arg_meta: null
129+
kernel_name: torch::executor::opt_upsample_nearest2d_vec_out

‎kernels/portable/cpu/op_upsample_nearest2d.cpp‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,95 @@
11
/*
22
* Copyright (c) Meta Platforms, Inc. and affiliates.
33
* All rights reserved.
4+
* Copyright 2026 Arm Limited and/or its affiliates.
45
*
56
* This source code is licensed under the BSD-style license found in the
67
* LICENSE file in the root directory of this source tree.
78
*/
89

910
#include <executorch/kernels/portable/cpu/util/upsample_util.h>
1011
#include <executorch/runtime/kernel/kernel_includes.h>
12+
#include <executorch/runtime/kernel/thread_parallel_interface.h>
13+
14+
#include <cstring>
1115

1216
namespace torch {
1317
namespace executor {
1418
namespace native {
1519

1620
namespace {
21+
22+
template <typename CTYPE>
23+
bool upsample_nearest2d_2x_nchw_fast_path(
24+
const Tensor& in,
25+
const float scale_h,
26+
const float scale_w,
27+
Tensor& out) {
28+
if (scale_h != 0.5f || scale_w != 0.5f || out.size(2) != in.size(2) * 2 ||
29+
out.size(3) != in.size(3) * 2) {
30+
return false;
31+
}
32+
33+
const int64_t in_h = in.size(2);
34+
const int64_t in_w = in.size(3);
35+
const int64_t out_h = out.size(2);
36+
const int64_t out_w = out.size(3);
37+
38+
const bool in_contiguous_nchw = in.strides()[3] == 1 &&
39+
in.strides()[2] == in_w && in.strides()[1] == in_h * in_w &&
40+
in.strides()[0] == in.size(1) * in_h * in_w;
41+
const bool out_contiguous_nchw = out.strides()[3] == 1 &&
42+
out.strides()[2] == out_w && out.strides()[1] == out_h * out_w &&
43+
out.strides()[0] == out.size(1) * out_h * out_w;
44+
if (!in_contiguous_nchw || !out_contiguous_nchw) {
45+
return false;
46+
}
47+
48+
const auto in_data = in.const_data_ptr<CTYPE>();
49+
auto out_data = out.mutable_data_ptr<CTYPE>();
50+
const int64_t planes = in.size(0) * in.size(1);
51+
const int64_t in_plane_size = in_h * in_w;
52+
const int64_t out_plane_size = out_h * out_w;
53+
int64_t grain_size =
54+
::executorch::extension::internal::GRAIN_SIZE / out_plane_size;
55+
if (grain_size < 1) {
56+
grain_size = 1;
57+
}
58+
59+
const bool success = ::executorch::extension::parallel_for(
60+
0, planes, grain_size, [&](const auto begin, const auto end) {
61+
for (const auto plane : c10::irange(begin, end)) {
62+
const CTYPE* in_plane = in_data + plane * in_plane_size;
63+
CTYPE* out_plane = out_data + plane * out_plane_size;
64+
65+
for (int64_t h = 0; h < in_h; ++h) {
66+
const CTYPE* in_row = in_plane + h * in_w;
67+
CTYPE* out_row0 = out_plane + (2 * h) * out_w;
68+
CTYPE* out_row1 = out_row0 + out_w;
69+
70+
for (int64_t w = 0; w < in_w; ++w) {
71+
const CTYPE value = in_row[w];
72+
out_row0[2 * w] = value;
73+
out_row0[2 * w + 1] = value;
74+
}
75+
std::memcpy(out_row1, out_row0, out_w * sizeof(CTYPE));
76+
}
77+
}
78+
});
79+
80+
return success;
81+
}
82+
1783
template <typename CTYPE>
1884
void upsample_nearest2d_kernel_impl_nchw(
1985
const Tensor& in,
2086
const float scale_h,
2187
const float scale_w,
2288
Tensor& out) {
89+
if (upsample_nearest2d_2x_nchw_fast_path<CTYPE>(in, scale_h, scale_w, out)) {
90+
return;
91+
}
92+
2393
const auto in_data = in.const_data_ptr<CTYPE>();
2494
auto out_data = out.mutable_data_ptr<CTYPE>();
2595

‎kernels/test/CMakeLists.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (c) Meta Platforms, Inc. and affiliates.
22
# All rights reserved.
3+
# Copyright 2026 Arm Limited and/or its affiliates.
34
#
45
# This source code is licensed under the BSD-style license found in the
56
# LICENSE file in the root directory of this source tree.
@@ -363,6 +364,7 @@ if(EXECUTORCH_BUILD_KERNELS_OPTIMIZED)
363364
"op_native_layer_norm_test.cpp"
364365
"op_neg_test.cpp"
365366
"op_sub_test.cpp"
367+
"op_upsample_nearest2d_test.cpp"
366368
"op_where_test.cpp"
367369
"UnaryUfuncRealHBBF16ToFloatHBF16Test.cpp"
368370
${CMAKE_CURRENT_BINARY_DIR}/include/optimized/executorch/kernels/test/supported_features.cpp

0 commit comments

Comments
 (0)