Skip to content

Commit 5869165

Browse files
Fix _upsample_bilinear2d_aa portable kernel correctness (pytorch#20733)
### Summary Fixes pytorch#13969 Fix _upsample_bilinear2d_aa portable kernel correctness for anti-aliased downsampling by removing the 4 tap contributor limit and matching PyTorch’s full support window. Added portable tests and corrected tolerance in test file cc @larryliu0820 @manuelcandales
1 parent e51b724 commit 5869165

3 files changed

Lines changed: 149 additions & 128 deletions

File tree

kernels/portable/cpu/op_upsample_bilinear2d_aa.cpp

Lines changed: 77 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,19 @@ inline T bilinear_aa_filter(T x) {
3030
: static_cast<T>(0.0);
3131
}
3232

33-
// Compute anti-aliasing weights exactly matching PyTorch's algorithm
3433
template <typename T>
35-
void compute_aa_weights_for_pixel(
36-
int64_t output_idx,
37-
T scale,
38-
int64_t input_size,
39-
int64_t* indices,
40-
T* weights,
41-
int64_t* num_contributors) {
42-
// Use the provided scale directly instead of recalculating
34+
struct AAKernelParams {
35+
int64_t xmin;
36+
int64_t xmax;
37+
int64_t fallback_index;
38+
T center;
39+
T invscale;
40+
T total_weight;
41+
};
4342

43+
template <typename T>
44+
AAKernelParams<T>
45+
compute_aa_kernel_params(int64_t output_idx, T scale, int64_t input_size) {
4446
// PyTorch's center calculation for anti-aliasing
4547
// Always uses scale * (i + 0.5) for anti-aliasing, regardless of
4648
// align_corners
@@ -58,58 +60,58 @@ void compute_aa_weights_for_pixel(
5860
static_cast<int64_t>(0));
5961
const int64_t xmax = std::min(
6062
static_cast<int64_t>(center + support + static_cast<T>(0.5)), input_size);
61-
62-
*num_contributors = std::min(xmax - xmin, static_cast<int64_t>(4));
63+
const T invscale = (scale >= static_cast<T>(1.0))
64+
? (static_cast<T>(1.0) / scale)
65+
: static_cast<T>(1.0);
6366

6467
// Ensure we have at least one contributor
65-
if (*num_contributors <= 0) {
66-
*num_contributors = 1;
67-
indices[0] = std::max(
68+
if (xmax <= xmin) {
69+
const int64_t fallback_index = std::max(
6870
static_cast<int64_t>(0),
6971
std::min(static_cast<int64_t>(center), input_size - 1));
70-
weights[0] = static_cast<T>(1.0);
71-
// Clear unused weight slots
72-
for (int64_t j = 1; j < 4; ++j) {
73-
weights[j] = static_cast<T>(0.0);
74-
}
75-
return;
72+
return {
73+
fallback_index,
74+
fallback_index + 1,
75+
fallback_index,
76+
center,
77+
invscale,
78+
static_cast<T>(1.0)};
7679
}
7780

78-
// PyTorch's weight computation
7981
T total_weight = static_cast<T>(0.0);
80-
const T invscale = (scale >= static_cast<T>(1.0))
81-
? (static_cast<T>(1.0) / scale)
82-
: static_cast<T>(1.0);
83-
84-
for (int64_t j = 0; j < *num_contributors; ++j) {
85-
int64_t x = xmin + j;
86-
// PyTorch's exact weight formula: (j + xmin - center + 0.5) * invscale
87-
T arg = (static_cast<T>(j) + static_cast<T>(xmin) - center +
88-
static_cast<T>(0.5)) *
89-
invscale;
90-
T weight = bilinear_aa_filter<T>(arg);
91-
indices[j] = x;
92-
weights[j] = weight;
93-
total_weight += weight;
82+
for (int64_t x = xmin; x < xmax; ++x) {
83+
total_weight += bilinear_aa_filter<T>(
84+
(static_cast<T>(x) - center + static_cast<T>(0.5)) * invscale);
9485
}
9586

96-
// Normalize weights to sum to 1 (PyTorch does this)
97-
if (total_weight > static_cast<T>(0.0)) {
98-
for (int64_t j = 0; j < *num_contributors; ++j) {
99-
weights[j] /= total_weight;
100-
}
101-
} else {
102-
// Fallback: if total weight is 0, set equal weights
103-
T equal_weight = static_cast<T>(1.0) / static_cast<T>(*num_contributors);
104-
for (int64_t j = 0; j < *num_contributors; ++j) {
105-
weights[j] = equal_weight;
106-
}
87+
if (total_weight <= static_cast<T>(0.0)) {
88+
const int64_t fallback_index = std::max(
89+
static_cast<int64_t>(0),
90+
std::min(static_cast<int64_t>(center), input_size - 1));
91+
return {
92+
fallback_index,
93+
fallback_index + 1,
94+
fallback_index,
95+
center,
96+
invscale,
97+
static_cast<T>(1.0)};
10798
}
10899

109-
// Clear unused weight slots
110-
for (int64_t j = *num_contributors; j < 4; ++j) {
111-
weights[j] = static_cast<T>(0.0);
100+
return {xmin, xmax, -1, center, invscale, total_weight};
101+
}
102+
103+
template <typename T>
104+
inline T compute_normalized_aa_weight(
105+
const AAKernelParams<T>& params,
106+
int64_t input_idx) {
107+
if (params.fallback_index >= 0) {
108+
return (input_idx == params.fallback_index) ? static_cast<T>(1.0)
109+
: static_cast<T>(0.0);
112110
}
111+
return bilinear_aa_filter<T>(
112+
(static_cast<T>(input_idx) - params.center + static_cast<T>(0.5)) *
113+
params.invscale) /
114+
params.total_weight;
113115
}
114116

115117
template <typename CTYPE>
@@ -129,48 +131,28 @@ void upsample_bilinear2d_aa_kernel_impl(
129131
if (is_nchw) {
130132
// NCHW layout
131133
for (int64_t n = 0; n < out.size(0); ++n) {
132-
for (int64_t c = 0; c < out.size(1); ++c) {
133-
const auto in_plane =
134-
in_data + (n * in.size(1) + c) * in.size(2) * in.size(3);
135-
auto out_plane =
136-
out_data + (n * out.size(1) + c) * out.size(2) * out.size(3);
137-
138-
for (int64_t oh = 0; oh < out.size(2); ++oh) {
139-
// Compute height weights for this output row
140-
int64_t h_indices[4];
141-
float h_weights[4];
142-
int64_t h_num_contributors;
143-
compute_aa_weights_for_pixel<float>(
144-
oh,
145-
scale_h,
146-
in.size(2),
147-
h_indices,
148-
h_weights,
149-
&h_num_contributors);
150-
151-
for (int64_t ow = 0; ow < out.size(3); ++ow) {
152-
// Compute width weights for this output column
153-
int64_t w_indices[4];
154-
float w_weights[4];
155-
int64_t w_num_contributors;
156-
compute_aa_weights_for_pixel<float>(
157-
ow,
158-
scale_w,
159-
in.size(3),
160-
w_indices,
161-
w_weights,
162-
&w_num_contributors);
134+
for (int64_t oh = 0; oh < out.size(2); ++oh) {
135+
const auto h_params =
136+
compute_aa_kernel_params<float>(oh, scale_h, in.size(2));
163137

138+
for (int64_t ow = 0; ow < out.size(3); ++ow) {
139+
const auto w_params =
140+
compute_aa_kernel_params<float>(ow, scale_w, in.size(3));
141+
142+
for (int64_t c = 0; c < out.size(1); ++c) {
143+
const auto in_plane =
144+
in_data + (n * in.size(1) + c) * in.size(2) * in.size(3);
145+
auto out_plane =
146+
out_data + (n * out.size(1) + c) * out.size(2) * out.size(3);
164147
CTYPE value = 0;
165148

166149
// Apply anti-aliased interpolation
167-
for (int64_t ih_idx = 0; ih_idx < h_num_contributors; ++ih_idx) {
168-
int64_t ih = h_indices[ih_idx];
169-
float h_weight = h_weights[ih_idx];
150+
for (int64_t ih = h_params.xmin; ih < h_params.xmax; ++ih) {
151+
const float h_weight = compute_normalized_aa_weight(h_params, ih);
170152

171-
for (int64_t iw_idx = 0; iw_idx < w_num_contributors; ++iw_idx) {
172-
int64_t iw = w_indices[iw_idx];
173-
float w_weight = w_weights[iw_idx];
153+
for (int64_t iw = w_params.xmin; iw < w_params.xmax; ++iw) {
154+
const float w_weight =
155+
compute_normalized_aa_weight(w_params, iw);
174156

175157
value += in_plane[ih * in.size(3) + iw] * h_weight * w_weight;
176158
}
@@ -188,37 +170,23 @@ void upsample_bilinear2d_aa_kernel_impl(
188170
auto out_batch = out_data + n * out.size(1) * out.size(2) * out.size(3);
189171

190172
for (int64_t oh = 0; oh < out.size(2); ++oh) {
191-
// Compute height weights for this output row
192-
int64_t h_indices[4];
193-
float h_weights[4];
194-
int64_t h_num_contributors;
195-
compute_aa_weights_for_pixel<float>(
196-
oh, scale_h, in.size(2), h_indices, h_weights, &h_num_contributors);
173+
const auto h_params =
174+
compute_aa_kernel_params<float>(oh, scale_h, in.size(2));
197175

198176
for (int64_t ow = 0; ow < out.size(3); ++ow) {
199-
// Compute width weights for this output column
200-
int64_t w_indices[4];
201-
float w_weights[4];
202-
int64_t w_num_contributors;
203-
compute_aa_weights_for_pixel<float>(
204-
ow,
205-
scale_w,
206-
in.size(3),
207-
w_indices,
208-
w_weights,
209-
&w_num_contributors);
177+
const auto w_params =
178+
compute_aa_kernel_params<float>(ow, scale_w, in.size(3));
210179

211180
for (int64_t c = 0; c < out.size(1); ++c) {
212181
CTYPE value = 0;
213182

214183
// Apply anti-aliased interpolation
215-
for (int64_t ih_idx = 0; ih_idx < h_num_contributors; ++ih_idx) {
216-
int64_t ih = h_indices[ih_idx];
217-
float h_weight = h_weights[ih_idx];
184+
for (int64_t ih = h_params.xmin; ih < h_params.xmax; ++ih) {
185+
const float h_weight = compute_normalized_aa_weight(h_params, ih);
218186

219-
for (int64_t iw_idx = 0; iw_idx < w_num_contributors; ++iw_idx) {
220-
int64_t iw = w_indices[iw_idx];
221-
float w_weight = w_weights[iw_idx];
187+
for (int64_t iw = w_params.xmin; iw < w_params.xmax; ++iw) {
188+
const float w_weight =
189+
compute_normalized_aa_weight(w_params, iw);
222190

223191
value += in_batch[(ih * in.size(3) + iw) * in.size(1) + c] *
224192
h_weight * w_weight;

kernels/portable/test/op_upsample_bilinear2d_aa_test.py

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,7 @@ def test_upsample_bilinear2d_aa_aggressive_downsampling(self):
161161
input_tensor,
162162
output_size=(2, 2),
163163
align_corners=False,
164-
# Aggressive 4x downsampling magnifies the separable-vs-direct
165-
# interpolation differences between ExecuTorch and ATen; observed
166-
# max abs error reaches ~0.6 for typical N(0,1) inputs.
167-
atol=1.0,
164+
atol=1e-3,
168165
)
169166

170167
def test_upsample_bilinear2d_aa_asymmetric_downsampling(self):
@@ -174,7 +171,7 @@ def test_upsample_bilinear2d_aa_asymmetric_downsampling(self):
174171
input_tensor,
175172
output_size=(4, 4), # 3x downsample in H, 2x in W
176173
align_corners=False,
177-
atol=0.25, # Relaxed tolerance due to implementation differences in separable vs direct interpolation
174+
atol=1e-3,
178175
)
179176

180177
def test_upsample_bilinear2d_aa_align_corners_upsampling(self):
@@ -194,7 +191,7 @@ def test_upsample_bilinear2d_aa_align_corners_downsampling(self):
194191
input_tensor,
195192
output_size=(4, 4),
196193
align_corners=True,
197-
atol=0.25, # Relaxed tolerance due to implementation differences in separable vs direct interpolation
194+
atol=1e-3,
198195
)
199196

200197
def test_upsample_bilinear2d_aa_batched(self):
@@ -245,19 +242,12 @@ def test_upsample_bilinear2d_aa_known_values_correctness(self):
245242
"""Test against known correct output values to catch regressions."""
246243
# This test case is adapted from ATen's test suite
247244
input_tensor = torch.arange(3 * 8 * 8, dtype=torch.float).reshape(1, 3, 8, 8)
248-
249-
# Test with a known downsampling case
250-
try:
251-
self.run_upsample_aa_test(
252-
input_tensor,
253-
output_size=(2, 2),
254-
align_corners=False,
255-
atol=1e-2, # Slightly relaxed for implementation differences
256-
)
257-
# The test should pass if our implementation is close to ATen
258-
except AssertionError as e:
259-
# Log the difference for debugging but don't fail the test during development
260-
print(f"Known values test difference (expected during development): {e}")
245+
self.run_upsample_aa_test(
246+
input_tensor,
247+
output_size=(2, 2),
248+
align_corners=False,
249+
atol=1e-3,
250+
)
261251

262252
def test_upsample_bilinear2d_aa_various_dtypes(self):
263253
"""Test with various data types."""

kernels/test/op_upsample_bilinear2d_aa_test.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,69 @@ TEST_F(OpUpsampleBilinear2dAAOutTest, TestPrecisionConsistency) {
626626
}
627627
}
628628

629+
TEST_F(OpUpsampleBilinear2dAAOutTest, Simple8x1To3x8) {
630+
TensorFactory<ScalarType::Float> tf;
631+
632+
Tensor input = tf.make(
633+
{1, 1, 8, 1},
634+
{-98.5, 49.875, 17.125, -46.5, 10.625, -95.875, -3.875, -4.625});
635+
Tensor out = tf.zeros({1, 1, 3, 8});
636+
637+
int64_t output_size_data[2] = {3, 8};
638+
ArrayRef<int64_t> output_size(output_size_data, 2);
639+
640+
op_upsample_bilinear2d_aa_out(
641+
input,
642+
output_size,
643+
/*align_corners=*/false,
644+
std::nullopt,
645+
std::nullopt,
646+
out);
647+
648+
auto expected = tf.zeros({1, 1, 3, 8});
649+
auto expected_data = expected.mutable_data_ptr<float>();
650+
const float expected_rows[] = {-8.440787f, -23.13393f, -24.736841f};
651+
for (int row = 0; row < 3; ++row) {
652+
for (int col = 0; col < 8; ++col) {
653+
expected_data[row * 8 + col] = expected_rows[row];
654+
}
655+
}
656+
657+
EXPECT_TENSOR_CLOSE_WITH_TOL(out, expected, 0, 1e-4);
658+
}
659+
660+
TEST_F(OpUpsampleBilinear2dAAOutTest, Simple8x1To3x8ChannelsLast) {
661+
TensorFactory<ScalarType::Float> tf;
662+
663+
Tensor input = tf.make_channels_last(
664+
{1, 1, 8, 1},
665+
{-98.5, 49.875, 17.125, -46.5, 10.625, -95.875, -3.875, -4.625});
666+
Tensor out = tf.zeros_channels_last({1, 1, 3, 8});
667+
668+
int64_t output_size_data[2] = {3, 8};
669+
ArrayRef<int64_t> output_size(output_size_data, 2);
670+
671+
op_upsample_bilinear2d_aa_out(
672+
input,
673+
output_size,
674+
/*align_corners=*/false,
675+
std::nullopt,
676+
std::nullopt,
677+
out);
678+
679+
auto expected_contiguous = tf.zeros({1, 1, 3, 8});
680+
auto expected_data = expected_contiguous.mutable_data_ptr<float>();
681+
const float expected_rows[] = {-8.440787f, -23.13393f, -24.736841f};
682+
for (int row = 0; row < 3; ++row) {
683+
for (int col = 0; col < 8; ++col) {
684+
expected_data[row * 8 + col] = expected_rows[row];
685+
}
686+
}
687+
const auto expected = tf.channels_last_like(expected_contiguous);
688+
689+
EXPECT_TENSOR_CLOSE_WITH_TOL(out, expected, 0, 1e-4);
690+
}
691+
629692
TEST_F(OpUpsampleBilinear2dAAOutTest, TestSpecificInputCase) {
630693
TensorFactory<ScalarType::Float> tf;
631694

0 commit comments

Comments
 (0)