Skip to content

Commit 4451779

Browse files
committed
Add aten::complex.out support for export and CPU execution
1 parent 558c071 commit 4451779

10 files changed

Lines changed: 344 additions & 0 deletions

File tree

‎exir/dialects/edge/dtype/supported.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
torch.float: "Float",
2828
torch.double: "Double",
2929
torch.uint16: "UInt16",
30+
torch.complex32: "ComplexHalf",
3031
torch.complex64: "ComplexFloat",
3132
torch.complex128: "ComplexDouble",
3233
}

‎exir/verification/verifier.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ def _get_core_aten_ops_exception_list(self) -> List[torch._ops.OpOverload]:
144144
torch.ops.aten.max.default, # TODO(T188268054)
145145
torch.ops.aten.min.default, # TODO(T188268054)
146146
torch.ops.aten.full_like.default, # TODO(T183507359)
147+
# TODO: Add complex.default to PyTorch's Core ATen opset.
148+
# Allow it here until the upstream change is available in supported versions.
149+
torch.ops.aten.complex.default,
147150
]
148151
+ list(_EXECUTORCH_SYM_OPS)
149152
+ DISALLOW_LIST

‎extension/pybindings/test/test_pybindings.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,6 +1838,24 @@ def test_non_dense_numpy_input_is_rejected(self):
18381838
with self.assertRaisesRegex(BufferError, "dense, non-overlapping"):
18391839
executorch_module(non_dense)
18401840

1841+
def test_complex_construction(self):
1842+
class Complex(torch.nn.Module):
1843+
def forward(self, real, imag):
1844+
return torch.complex(real, imag)
1845+
1846+
for dtype in (torch.float16, torch.float32, torch.float64):
1847+
with self.subTest(dtype=dtype):
1848+
real = torch.tensor([[1.0], [-2.0]], dtype=dtype)
1849+
imag = torch.tensor([[3.0, 0.0, -4.0]], dtype=dtype)
1850+
program = to_edge(export(Complex(), (real, imag))).to_executorch()
1851+
module = self.load_fn(program.buffer)
1852+
output = torch.from_dlpack(module.forward([real, imag])[0])
1853+
expected = torch.complex(real, imag)
1854+
self.assertEqual(output.dtype, expected.dtype)
1855+
self.assertEqual(output.shape, expected.shape)
1856+
torch.testing.assert_close(output.real, expected.real, rtol=0, atol=0)
1857+
torch.testing.assert_close(output.imag, expected.imag, rtol=0, atol=0)
1858+
18411859
def test_complex_numpy_input(self):
18421860
class Identity(torch.nn.Module):
18431861
def forward(self, x):

‎kernels/aten/functions.yaml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@
121121

122122
- op: clone.out
123123

124+
- op: complex.out
125+
124126
- op: constant_pad_nd.out
125127

126128
- op: conv1d.out
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 <executorch/kernels/portable/cpu/util/broadcast_util.h>
10+
#include <executorch/runtime/kernel/kernel_includes.h>
11+
12+
namespace torch {
13+
namespace executor {
14+
namespace native {
15+
16+
Tensor& complex_out(
17+
KernelRuntimeContext& ctx,
18+
const Tensor& real,
19+
const Tensor& imag,
20+
Tensor& out) {
21+
// Check Input Dtypes
22+
const ScalarType dtype = real.scalar_type();
23+
ET_KERNEL_CHECK(
24+
ctx,
25+
dtype == ScalarType::Half || dtype == ScalarType::Float ||
26+
dtype == ScalarType::Double,
27+
InvalidArgument,
28+
out);
29+
ET_KERNEL_CHECK(ctx, imag.scalar_type() == dtype, InvalidArgument, out);
30+
31+
// Check Output Dtype
32+
ET_KERNEL_CHECK(
33+
ctx,
34+
out.scalar_type() == executorch::runtime::toComplexType(dtype),
35+
InvalidArgument,
36+
out);
37+
38+
// Check Dim Order
39+
ET_KERNEL_CHECK(
40+
ctx,
41+
tensor_is_default_dim_order(real) && tensor_is_default_dim_order(imag) &&
42+
tensor_is_default_dim_order(out),
43+
InvalidArgument,
44+
out);
45+
46+
// Resize
47+
ET_KERNEL_CHECK(
48+
ctx,
49+
resize_to_broadcast_target_size(real, imag, out) == Error::Ok,
50+
InvalidArgument,
51+
out);
52+
53+
ET_SWITCH_FLOATH_TYPES(dtype, ctx, "complex.out", CTYPE, [&] {
54+
using Complex = executorch::aten::complex<CTYPE>;
55+
apply_binary_elementwise_fn<CTYPE, CTYPE, Complex>(
56+
[](CTYPE real_value, CTYPE imag_value) {
57+
return Complex(real_value, imag_value);
58+
},
59+
real,
60+
imag,
61+
out);
62+
});
63+
return out;
64+
}
65+
66+
} // namespace native
67+
} // namespace executor
68+
} // namespace torch

‎kernels/portable/functions.yaml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,11 @@
288288
- arg_meta: null
289289
kernel_name: torch::executor::clone_out
290290

291+
- op: complex.out
292+
kernels:
293+
- arg_meta: null
294+
kernel_name: torch::executor::complex_out
295+
291296
- op: constant_pad_nd.out
292297
kernels:
293298
- arg_meta: null

‎kernels/test/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ set(all_test_sources
191191
"op_ceil_test.cpp"
192192
"op_clamp_test.cpp"
193193
"op_clone_test.cpp"
194+
"op_complex_test.cpp"
194195
"op_constant_pad_nd_test.cpp"
195196
"op_convolution_backward_test.cpp"
196197
"op_convolution_test.cpp"

‎kernels/test/op_complex_test.cpp‎

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
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 <executorch/kernels/test/FunctionHeaderWrapper.h>
10+
#include <executorch/kernels/test/TestUtil.h>
11+
#include <executorch/runtime/core/exec_aten/testing_util/tensor_factory.h>
12+
#include <executorch/runtime/core/exec_aten/testing_util/tensor_util.h>
13+
14+
#include <cmath>
15+
#include <limits>
16+
17+
using executorch::aten::ScalarType;
18+
using executorch::aten::Tensor;
19+
using executorch::aten::TensorShapeDynamism;
20+
using torch::executor::testing::TensorFactory;
21+
22+
class OpComplexOutTest : public OperatorTest {
23+
protected:
24+
Tensor& op_complex_out(const Tensor& real, const Tensor& imag, Tensor& out) {
25+
return torch::executor::aten::complex_outf(context_, real, imag, out);
26+
}
27+
28+
template <ScalarType REAL, ScalarType COMPLEX>
29+
void test_dtype() {
30+
TensorFactory<REAL> tf;
31+
TensorFactory<COMPLEX> cf;
32+
using C = typename TensorFactory<COMPLEX>::true_ctype;
33+
Tensor real = tf.make({2, 2}, {1, -2, 0, 4});
34+
Tensor imag = tf.make({2, 2}, {-5, 0, 7, -8});
35+
Tensor out = cf.full({2, 2}, C(0, 0));
36+
EXPECT_EQ(&op_complex_out(real, imag, out), &out);
37+
EXPECT_TENSOR_EQ(
38+
out, cf.make({2, 2}, {C(1, -5), C(-2, 0), C(0, 7), C(4, -8)}));
39+
}
40+
41+
template <ScalarType DTYPE>
42+
void test_unsupported_input() {
43+
TensorFactory<DTYPE> tf;
44+
TensorFactory<ScalarType::ComplexFloat> cf;
45+
Tensor input = tf.zeros({2});
46+
Tensor out = cf.zeros({2});
47+
ET_EXPECT_KERNEL_FAILURE(context_, op_complex_out(input, input, out));
48+
}
49+
};
50+
51+
TEST_F(OpComplexOutTest, Half) {
52+
test_dtype<ScalarType::Half, ScalarType::ComplexHalf>();
53+
}
54+
55+
TEST_F(OpComplexOutTest, Float) {
56+
test_dtype<ScalarType::Float, ScalarType::ComplexFloat>();
57+
}
58+
59+
TEST_F(OpComplexOutTest, Double) {
60+
test_dtype<ScalarType::Double, ScalarType::ComplexDouble>();
61+
}
62+
63+
TEST_F(OpComplexOutTest, SpecialValues) {
64+
TensorFactory<ScalarType::Float> tf;
65+
TensorFactory<ScalarType::ComplexFloat> cf;
66+
const float inf = std::numeric_limits<float>::infinity();
67+
const float nan = std::numeric_limits<float>::quiet_NaN();
68+
Tensor real = tf.make({4}, {0.0f, -0.0f, inf, nan});
69+
Tensor imag = tf.make({4}, {-0.0f, 0.0f, nan, -inf});
70+
Tensor out = cf.zeros({4});
71+
op_complex_out(real, imag, out);
72+
const auto* data = out.const_data_ptr<executorch::aten::complex<float>>();
73+
EXPECT_EQ(data[0].real(), 0.0f);
74+
EXPECT_FALSE(std::signbit(data[0].real()));
75+
EXPECT_EQ(data[0].imag(), 0.0f);
76+
EXPECT_TRUE(std::signbit(data[0].imag()));
77+
EXPECT_EQ(data[1].real(), 0.0f);
78+
EXPECT_TRUE(std::signbit(data[1].real()));
79+
EXPECT_EQ(data[1].imag(), 0.0f);
80+
EXPECT_FALSE(std::signbit(data[1].imag()));
81+
EXPECT_EQ(data[2].real(), inf);
82+
EXPECT_TRUE(std::isnan(data[2].imag()));
83+
EXPECT_TRUE(std::isnan(data[3].real()));
84+
EXPECT_EQ(data[3].imag(), -inf);
85+
}
86+
87+
TEST_F(OpComplexOutTest, BroadcastBothInputs) {
88+
TensorFactory<ScalarType::Float> tf;
89+
TensorFactory<ScalarType::ComplexFloat> cf;
90+
using C = executorch::aten::complex<float>;
91+
Tensor real = tf.make({2, 1}, {1, 2});
92+
Tensor imag = tf.make({1, 3}, {3, 4, 5});
93+
Tensor out = cf.zeros({2, 3});
94+
op_complex_out(real, imag, out);
95+
EXPECT_TENSOR_EQ(
96+
out,
97+
cf.make({2, 3}, {C(1, 3), C(1, 4), C(1, 5), C(2, 3), C(2, 4), C(2, 5)}));
98+
}
99+
100+
TEST_F(OpComplexOutTest, BroadcastDifferentRanks) {
101+
TensorFactory<ScalarType::Float> tf;
102+
TensorFactory<ScalarType::ComplexFloat> cf;
103+
using C = executorch::aten::complex<float>;
104+
Tensor real = tf.make({2}, {1, 2});
105+
Tensor imag = tf.make({2, 1}, {3, 4});
106+
Tensor out = cf.zeros({2, 2});
107+
op_complex_out(real, imag, out);
108+
EXPECT_TENSOR_EQ(out, cf.make({2, 2}, {C(1, 3), C(2, 3), C(1, 4), C(2, 4)}));
109+
}
110+
111+
TEST_F(OpComplexOutTest, Scalars) {
112+
TensorFactory<ScalarType::Float> tf;
113+
TensorFactory<ScalarType::ComplexFloat> cf;
114+
Tensor real = tf.make({}, {1});
115+
Tensor imag = tf.make({}, {-2});
116+
Tensor out = cf.zeros({});
117+
op_complex_out(real, imag, out);
118+
EXPECT_TENSOR_EQ(out, cf.make({}, {executorch::aten::complex<float>(1, -2)}));
119+
}
120+
121+
TEST_F(OpComplexOutTest, BroadcastScalar) {
122+
TensorFactory<ScalarType::Float> tf;
123+
TensorFactory<ScalarType::ComplexFloat> cf;
124+
using C = executorch::aten::complex<float>;
125+
Tensor real = tf.make({}, {1});
126+
Tensor imag = tf.make({2}, {2, 3});
127+
Tensor out = cf.zeros({2});
128+
op_complex_out(real, imag, out);
129+
EXPECT_TENSOR_EQ(out, cf.make({2}, {C(1, 2), C(1, 3)}));
130+
}
131+
132+
TEST_F(OpComplexOutTest, EmptyBroadcast) {
133+
TensorFactory<ScalarType::Float> tf;
134+
TensorFactory<ScalarType::ComplexFloat> cf;
135+
Tensor real = tf.zeros({0, 3});
136+
Tensor imag = tf.ones({1, 3});
137+
Tensor out = cf.zeros({0, 3});
138+
op_complex_out(real, imag, out);
139+
EXPECT_TENSOR_EQ(out, cf.zeros({0, 3}));
140+
}
141+
142+
TEST_F(OpComplexOutTest, DynamicOutput) {
143+
TensorFactory<ScalarType::Float> tf;
144+
TensorFactory<ScalarType::ComplexFloat> cf;
145+
using C = executorch::aten::complex<float>;
146+
Tensor real = tf.make({2, 1}, {1, 2});
147+
Tensor imag = tf.make({2}, {3, 4});
148+
Tensor out = cf.zeros({4, 4}, TensorShapeDynamism::DYNAMIC_BOUND);
149+
op_complex_out(real, imag, out);
150+
EXPECT_TENSOR_EQ(out, cf.make({2, 2}, {C(1, 3), C(1, 4), C(2, 3), C(2, 4)}));
151+
}
152+
153+
TEST_F(OpComplexOutTest, MismatchedInputs) {
154+
TensorFactory<ScalarType::Float> tf;
155+
TensorFactory<ScalarType::Double> df;
156+
TensorFactory<ScalarType::ComplexFloat> cf;
157+
Tensor real = tf.ones({2});
158+
Tensor imag = df.ones({2});
159+
Tensor out = cf.zeros({2});
160+
ET_EXPECT_KERNEL_FAILURE(context_, op_complex_out(real, imag, out));
161+
}
162+
163+
TEST_F(OpComplexOutTest, WrongComplexOutputType) {
164+
TensorFactory<ScalarType::Float> tf;
165+
TensorFactory<ScalarType::ComplexDouble> cf;
166+
Tensor input = tf.ones({2});
167+
Tensor out = cf.zeros({2});
168+
ET_EXPECT_KERNEL_FAILURE(context_, op_complex_out(input, input, out));
169+
}
170+
171+
TEST_F(OpComplexOutTest, RealOutputType) {
172+
TensorFactory<ScalarType::Float> tf;
173+
Tensor input = tf.ones({2});
174+
Tensor out = tf.zeros({2});
175+
ET_EXPECT_KERNEL_FAILURE(context_, op_complex_out(input, input, out));
176+
}
177+
178+
TEST_F(OpComplexOutTest, BFloat16Input) {
179+
test_unsupported_input<ScalarType::BFloat16>();
180+
}
181+
182+
TEST_F(OpComplexOutTest, IntegerInput) {
183+
test_unsupported_input<ScalarType::Int>();
184+
}
185+
186+
TEST_F(OpComplexOutTest, BoolInput) {
187+
test_unsupported_input<ScalarType::Bool>();
188+
}
189+
190+
TEST_F(OpComplexOutTest, ComplexInput) {
191+
test_unsupported_input<ScalarType::ComplexFloat>();
192+
}
193+
194+
TEST_F(OpComplexOutTest, IncompatibleShapes) {
195+
TensorFactory<ScalarType::Float> tf;
196+
TensorFactory<ScalarType::ComplexFloat> cf;
197+
Tensor real = tf.ones({2});
198+
Tensor imag = tf.ones({3});
199+
Tensor out = cf.zeros({3});
200+
ET_EXPECT_KERNEL_FAILURE(context_, op_complex_out(real, imag, out));
201+
}
202+
203+
#ifndef USE_ATEN_LIB
204+
TEST_F(OpComplexOutTest, OutputCapacityTooSmall) {
205+
TensorFactory<ScalarType::Float> tf;
206+
TensorFactory<ScalarType::ComplexFloat> cf;
207+
Tensor input = tf.ones({2, 3});
208+
Tensor out = cf.zeros({1, 1}, TensorShapeDynamism::DYNAMIC_BOUND);
209+
ET_EXPECT_KERNEL_FAILURE(context_, op_complex_out(input, input, out));
210+
}
211+
212+
TEST_F(OpComplexOutTest, StaticOutputWrongShape) {
213+
TensorFactory<ScalarType::Float> tf;
214+
TensorFactory<ScalarType::ComplexFloat> cf;
215+
Tensor input = tf.ones({2, 3});
216+
Tensor out = cf.zeros({3, 2});
217+
ET_EXPECT_KERNEL_FAILURE(context_, op_complex_out(input, input, out));
218+
}
219+
220+
TEST_F(OpComplexOutTest, NonDefaultDimOrder) {
221+
TensorFactory<ScalarType::Float> tf;
222+
TensorFactory<ScalarType::ComplexFloat> cf;
223+
for (int index = 0; index < 3; ++index) {
224+
context_ = {};
225+
Tensor real = tf.ones({2, 3});
226+
Tensor imag = tf.ones({2, 3});
227+
Tensor out = cf.zeros({2, 3});
228+
if (index == 0) {
229+
real = tf.make_with_dimorder({2, 3}, {1, 2, 3, 4, 5, 6}, {1, 0});
230+
} else if (index == 1) {
231+
imag = tf.make_with_dimorder({2, 3}, {1, 2, 3, 4, 5, 6}, {1, 0});
232+
} else {
233+
out = cf.make_with_dimorder(
234+
{2, 3}, std::vector<executorch::aten::complex<float>>(6), {1, 0});
235+
}
236+
ET_EXPECT_KERNEL_FAILURE(context_, op_complex_out(real, imag, out));
237+
}
238+
}
239+
#endif

‎kernels/test/targets.bzl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ def define_common_targets():
242242
_common_op_test("op_ceil_test", ["aten", "portable"])
243243
_common_op_test("op_clamp_test", ["aten", "portable"])
244244
_common_op_test("op_clone_test", ["aten", "portable"])
245+
_common_op_test("op_complex_test", ["aten", "portable"])
245246
_common_op_test("op_constant_pad_nd_test", ["aten", "portable"])
246247
_common_op_test("op_convolution_test", ["aten", "portable"])
247248
_common_op_test("op_convolution_backward_test", ["aten", "portable"])

0 commit comments

Comments
 (0)