diff --git a/scripts/generate_wrappers.py b/scripts/generate_wrappers.py index 89e31016f..1a356e7c0 100644 --- a/scripts/generate_wrappers.py +++ b/scripts/generate_wrappers.py @@ -490,6 +490,26 @@ def _find_optional_vector_int64_params(op_name): ) +def _vector_int64_kind(arg, vector_params, optional_vector_params): + """Classify a vector parameter, preferring its overload-local type.""" + spelling = "".join(arg.type.spelling.split()) + spelling = spelling.removeprefix("const").removesuffix("&") + + if spelling == "std::optional>": + return "optional" + + if spelling == "std::vector": + return "vector" + + if arg.spelling in optional_vector_params: + return "optional" + + if arg.spelling in vector_params: + return "vector" + + return None + + def _find_tensor_params(op_name): source = _find_base_header(op_name).read_text() @@ -545,7 +565,10 @@ def _is_optional(arg): return "std::optional" in arg.type.spelling def _is_optional_vector_int64(arg): - return arg.spelling in optional_vector_int64_params + return ( + _vector_int64_kind(arg, vector_int64_params, optional_vector_int64_params) + == "optional" + ) def _is_vector_tensor(arg): if arg.spelling in vector_tensor_params: @@ -554,7 +577,10 @@ def _is_vector_tensor(arg): return "std::vector" in arg.type.spelling and "Tensor" in arg.type.spelling def _is_vector_int64(arg): - return arg.spelling in vector_int64_params + return ( + _vector_int64_kind(arg, vector_int64_params, optional_vector_int64_params) + == "vector" + ) def _is_data_type(arg): return _is_data_type_spelling(arg.type.spelling) @@ -1002,7 +1028,10 @@ def _is_optional_tensor(arg): return arg.spelling in optional_tensor_params def _is_optional_vector_int64(arg): - return arg.spelling in optional_vector_int64_params + return ( + _vector_int64_kind(arg, vector_int64_params, optional_vector_int64_params) + == "optional" + ) def _is_vector_tensor(arg): if arg.spelling in vector_tensor_params: @@ -1011,7 +1040,10 @@ def _is_vector_tensor(arg): return "std::vector" in arg.type.spelling and "Tensor" in arg.type.spelling def _is_vector_int64(arg): - return arg.spelling in vector_int64_params + return ( + _vector_int64_kind(arg, vector_int64_params, optional_vector_int64_params) + == "vector" + ) def _is_tensor(arg): if arg.spelling in optional_non_tensor_params: @@ -1239,7 +1271,10 @@ def _is_optional_tensor(arg): return False def _is_optional_vector_int64(arg): - return arg.spelling in optional_vector_int64_params + return ( + _vector_int64_kind(arg, vector_int64_params, optional_vector_int64_params) + == "optional" + ) def _is_vector_tensor(arg): if arg.spelling in vector_tensor_params: @@ -1250,7 +1285,10 @@ def _is_vector_tensor(arg): ) def _is_vector_int64(arg): - return arg.spelling in vector_int64_params + return ( + _vector_int64_kind(arg, vector_int64_params, optional_vector_int64_params) + == "vector" + ) def _is_tensor(arg): if arg.spelling in optional_non_tensor_params: diff --git a/scripts/torch_ops.yaml b/scripts/torch_ops.yaml index 3f79960a0..c2b70bed8 100644 --- a/scripts/torch_ops.yaml +++ b/scripts/torch_ops.yaml @@ -266,7 +266,6 @@ - max_pool2d_with_indices_backward - max_pool3d_with_indices - max_pool3d_with_indices_backward -- max_unpool2d - max_unpool3d - maximum - mean diff --git a/src/base/detail/max_unpool.h b/src/base/detail/max_unpool.h new file mode 100644 index 000000000..60d23102c --- /dev/null +++ b/src/base/detail/max_unpool.h @@ -0,0 +1,78 @@ +#ifndef INFINI_OPS_BASE_DETAIL_MAX_UNPOOL_H_ +#define INFINI_OPS_BASE_DETAIL_MAX_UNPOOL_H_ + +#include +#include +#include +#include +#include +#include + +#include "operator.h" + +namespace infini::ops::max_unpool_detail { + +template +inline std::pair, std::vector> ResolveGeometry( + const Tensor input, const std::vector& kernel_size, + const std::optional>& stride, + const std::vector& padding, + const std::optional>& output_size) { + assert((input.ndim() == SpatialDimensions + 1 || + input.ndim() == SpatialDimensions + 2) && + "`MaxUnpool` input rank must include the spatial dimensions"); + assert(kernel_size.size() == SpatialDimensions && + "`MaxUnpool` `kernel_size` has the wrong length"); + assert(padding.size() == SpatialDimensions && + "`MaxUnpool` `padding` has the wrong length"); + + auto resolved_stride = stride.value_or(kernel_size); + assert(resolved_stride.size() == SpatialDimensions && + "`MaxUnpool` `stride` has the wrong length"); + + std::vector default_size(SpatialDimensions); + + for (std::size_t dim = 0; dim < SpatialDimensions; ++dim) { + assert(kernel_size[dim] > 0 && + "`MaxUnpool` requires positive `kernel_size` values"); + assert(resolved_stride[dim] > 0 && + "`MaxUnpool` requires positive `stride` values"); + assert(padding[dim] >= 0 && + "`MaxUnpool` requires non-negative `padding` values"); + + const auto input_size = static_cast( + input.size(input.ndim() - SpatialDimensions + dim)); + default_size[dim] = (input_size - 1) * resolved_stride[dim] + + kernel_size[dim] - 2 * padding[dim]; + } + + auto resolved_output_size = output_size.value_or(default_size); + + if (output_size.has_value()) { + if (resolved_output_size.size() == SpatialDimensions + 2) { + resolved_output_size.erase(resolved_output_size.begin(), + resolved_output_size.begin() + 2); + } + + assert(resolved_output_size.size() == SpatialDimensions && + "`MaxUnpool` `output_size` has the wrong length"); + + for (std::size_t dim = 0; dim < SpatialDimensions; ++dim) { + const auto min_size = default_size[dim] - resolved_stride[dim]; + const auto max_size = default_size[dim] + resolved_stride[dim]; + assert((min_size < resolved_output_size[dim] && + resolved_output_size[dim] < max_size) && + "`MaxUnpool` `output_size` is outside the valid range"); + } + } + + for (const auto value : resolved_output_size) { + assert(value >= 0 && "`MaxUnpool` requires non-negative output dimensions"); + } + + return {std::move(resolved_output_size), std::move(resolved_stride)}; +} + +} // namespace infini::ops::max_unpool_detail + +#endif diff --git a/src/base/max_unpool2d.h b/src/base/max_unpool2d.h index 8b6dbfd35..025048958 100644 --- a/src/base/max_unpool2d.h +++ b/src/base/max_unpool2d.h @@ -1,16 +1,21 @@ #ifndef INFINI_OPS_BASE_MAX_UNPOOL2D_H_ #define INFINI_OPS_BASE_MAX_UNPOOL2D_H_ +#include +#include #include -#include "operator.h" +#include "detail/max_unpool.h" namespace infini::ops { class MaxUnpool2d : public Operator { public: MaxUnpool2d(const Tensor input, const Tensor indices, - const std::vector output_size, Tensor out) + const std::vector kernel_size, + const std::optional> stride, + const std::vector padding, + const std::optional> output_size, Tensor out) : input_shape_{input.shape()}, input_strides_{input.strides()}, input_type_{input.dtype()}, @@ -20,14 +25,29 @@ class MaxUnpool2d : public Operator { out_shape_{out.shape()}, out_strides_{out.strides()}, out_type_{out.dtype()}, - output_size_{output_size}, - device_index_{out.device().index()} {} - - virtual void operator()(const Tensor input, const Tensor indices, - const std::vector output_size, - Tensor out) const = 0; + output_size_{}, + device_index_{out.device().index()} { + auto geometry = max_unpool_detail::ResolveGeometry<2>( + input, kernel_size, stride, padding, output_size); + output_size_ = std::move(geometry.first); + } + + void operator()(const Tensor input, const Tensor indices, + const std::vector kernel_size, + const std::optional> stride, + const std::vector padding, + const std::optional> output_size, + Tensor out) const { + auto geometry = max_unpool_detail::ResolveGeometry<2>( + input, kernel_size, stride, padding, output_size); + Run(input, indices, std::move(geometry.first), out); + } protected: + virtual void Run(const Tensor input, const Tensor indices, + const std::vector output_size, + Tensor out) const = 0; + Tensor::Shape input_shape_; Tensor::Strides input_strides_; diff --git a/src/base/max_unpool3d.h b/src/base/max_unpool3d.h index 62953ba9f..0992e056c 100644 --- a/src/base/max_unpool3d.h +++ b/src/base/max_unpool3d.h @@ -1,14 +1,43 @@ #ifndef INFINI_OPS_BASE_MAX_UNPOOL3D_H_ #define INFINI_OPS_BASE_MAX_UNPOOL3D_H_ +#include +#include #include -#include "operator.h" +#include "detail/max_unpool.h" namespace infini::ops { class MaxUnpool3d : public Operator { public: + MaxUnpool3d(const Tensor input, const Tensor indices, + const std::vector kernel_size, + const std::optional> stride, + const std::vector padding, + const std::optional> output_size, Tensor out) + : input_shape_{input.shape()}, + input_strides_{input.strides()}, + input_type_{input.dtype()}, + indices_shape_{indices.shape()}, + indices_strides_{indices.strides()}, + indices_type_{indices.dtype()}, + out_shape_{out.shape()}, + out_strides_{out.strides()}, + out_type_{out.dtype()}, + output_size_{}, + stride_{}, + padding_{padding}, + device_index_{out.device().index()} { + auto geometry = max_unpool_detail::ResolveGeometry<3>( + input, kernel_size, stride, padding, output_size); + output_size_ = std::move(geometry.first); + stride_ = std::move(geometry.second); + } + + /// \deprecated Use the overload that accepts `kernel_size`, `stride`, + /// `padding`, and `output_size` instead. + [[deprecated("Use the `kernel_size` overload instead.")]] MaxUnpool3d(const Tensor input, const Tensor indices, const std::vector output_size, const std::vector stride, @@ -27,11 +56,24 @@ class MaxUnpool3d : public Operator { padding_{padding}, device_index_{out.device().index()} {} - virtual void operator()(const Tensor input, const Tensor indices, - const std::vector output_size, - const std::vector stride, - const std::vector padding, - Tensor out) const = 0; + void operator()(const Tensor input, const Tensor indices, + const std::vector kernel_size, + const std::optional> stride, + const std::vector padding, + const std::optional> output_size, + Tensor out) const { + const auto geometry = max_unpool_detail::ResolveGeometry<3>( + input, kernel_size, stride, padding, output_size); + (*this)(input, indices, geometry.first, geometry.second, padding, out); + } + + /// \deprecated Use the overload that accepts `kernel_size`, `stride`, + /// `padding`, and `output_size` instead. + [[deprecated("Use the `kernel_size` overload instead.")]] virtual void + operator()(const Tensor input, const Tensor indices, + const std::vector output_size, + const std::vector stride, + const std::vector padding, Tensor out) const = 0; protected: Tensor::Shape input_shape_; diff --git a/src/torch/ops/max_unpool2d/max_unpool2d.cc b/src/torch/ops/max_unpool2d/max_unpool2d.cc new file mode 100644 index 000000000..c875ab397 --- /dev/null +++ b/src/torch/ops/max_unpool2d/max_unpool2d.cc @@ -0,0 +1,50 @@ +#include "torch/ops/max_unpool2d/max_unpool2d.h" + +#include "torch/tensor_.h" + +namespace infini::ops { + +template +void Operator::Run(const Tensor input, + const Tensor indices, + const std::vector output_size, + Tensor out) const { + const auto device_index = out.device().index(); + auto at_input = + ToAtenTensor(const_cast(input.data()), input_shape_, + input_strides_, input_type_, device_index); + auto at_indices = + ToAtenTensor(const_cast(indices.data()), indices_shape_, + indices_strides_, indices_type_, device_index); + auto at_out = ToAtenTensor(out.data(), out_shape_, out_strides_, + out_type_, device_index); + + at::max_unpool2d_out(at_out, at_input, at_indices, output_size); +} + +#ifdef WITH_CPU +template class Operator; +#endif +#ifdef WITH_NVIDIA +template class Operator; +#endif +#ifdef WITH_CAMBRICON +template class Operator; +#endif +#ifdef WITH_ASCEND +template class Operator; +#endif +#ifdef WITH_METAX +template class Operator; +#endif +#ifdef WITH_MOORE +template class Operator; +#endif +#ifdef WITH_ILUVATAR +template class Operator; +#endif +#ifdef WITH_HYGON +template class Operator; +#endif + +} // namespace infini::ops diff --git a/src/torch/ops/max_unpool2d/max_unpool2d.h b/src/torch/ops/max_unpool2d/max_unpool2d.h new file mode 100644 index 000000000..507f9f2fb --- /dev/null +++ b/src/torch/ops/max_unpool2d/max_unpool2d.h @@ -0,0 +1,20 @@ +#ifndef INFINI_OPS_TORCH_MAX_UNPOOL2D_H_ +#define INFINI_OPS_TORCH_MAX_UNPOOL2D_H_ + +#include "base/max_unpool2d.h" + +namespace infini::ops { + +template +class Operator : public MaxUnpool2d { + public: + using MaxUnpool2d::MaxUnpool2d; + + protected: + void Run(const Tensor input, const Tensor indices, + const std::vector output_size, Tensor out) const override; +}; + +} // namespace infini::ops + +#endif diff --git a/tests/test_generate_wrappers.py b/tests/test_generate_wrappers.py index 7c033f8cc..43b85ee10 100644 --- a/tests/test_generate_wrappers.py +++ b/tests/test_generate_wrappers.py @@ -190,6 +190,80 @@ class [[deprecated("Use HistogramV2 instead.")]] Histogram { assert "Call" not in text +def test_vector_classification_is_overload_local_and_preserves_nesting( + monkeypatch, tmp_path +): + module = _load_generator_module() + base_header = tmp_path / "max_unpool3d.h" + base_header.write_text( + """ +class MaxUnpool3d { + public: + void operator()(const Tensor input, const Tensor indices, + const std::vector kernel_size, + const std::optional> stride, + const std::vector padding, + const std::optional> output_size, + Tensor out) const; + virtual void operator()(const Tensor input, const Tensor indices, + const std::vector output_size, + const std::vector stride, + const std::vector padding, + Tensor out) const = 0; +}; +""" + ) + monkeypatch.setattr(module, "_find_base_header", lambda op_name: base_header) + + canonical = module._ParsedFunction( + [ + module._ParsedArgument("const Tensor", "input"), + module._ParsedArgument("const Tensor", "indices"), + module._ParsedArgument("const std::vector", "kernel_size"), + module._ParsedArgument( + "const std::optional>", "stride" + ), + module._ParsedArgument("const std::vector", "padding"), + module._ParsedArgument( + "const std::optional>", "output_size" + ), + module._ParsedArgument("Tensor", "out"), + ] + ) + legacy = module._ParsedFunction( + [ + module._ParsedArgument("const Tensor", "input"), + module._ParsedArgument("const Tensor", "indices"), + module._ParsedArgument("const std::vector", "output_size"), + module._ParsedArgument("const std::vector", "stride"), + module._ParsedArgument("const std::vector", "padding"), + module._ParsedArgument("Tensor", "out"), + ] + ) + nested = module._ParsedFunction( + [ + module._ParsedArgument("const Tensor", "a"), + module._ParsedArgument("const Tensor", "b"), + module._ParsedArgument("const std::vector>", "dims"), + module._ParsedArgument("Tensor", "out"), + ] + ) + operator = module._Operator( + "max_unpool3d", constructors=[], calls=[canonical, legacy, nested] + ) + + binding = module._generate_pybind11(operator) + dispatch, _ = module._generate_generated_dispatch_entries(operator) + instantiations, _ = module._generate_operator_call_instantiation_entries(operator) + generated = "\n".join((binding, *dispatch, *instantiations)) + + assert "const std::optional> stride" in generated + assert "const std::optional> output_size" in generated + assert "const std::vector stride" in generated + assert "const std::vector output_size" in generated + assert "const std::vector> dims" in generated + + def test_pybind_default_implementation_uses_first_active_index(monkeypatch, tmp_path): module = _load_generator_module() base_header = tmp_path / "mul.h" diff --git a/tests/test_torch_ops.py b/tests/test_torch_ops.py index 99021467d..f046f9169 100644 --- a/tests/test_torch_ops.py +++ b/tests/test_torch_ops.py @@ -285,8 +285,6 @@ def _list_default(aten_type): "thnn_conv2d", "im2col", "col2im", - "max_unpool2d", - "max_unpool3d", "reflection_pad1d", "reflection_pad2d", "reflection_pad3d", @@ -413,9 +411,11 @@ def _build_input_value(op_name, param, shape, dtype, device, tensor_idx): return _TYPE_DEFAULTS.get(t, 0.5) -def _call_infini(op_name, *args): +def _call_infini(op_name, *args, **kwargs): try: - getattr(infini.ops, op_name)(*args, implementation_index=_PYTORCH_SLOT) + getattr(infini.ops, op_name)( + *args, **kwargs, implementation_index=_PYTORCH_SLOT + ) except RuntimeError as exc: if any(p in str(exc) for p in _VENDOR_SKIP_PATTERNS): pytest.skip(f"`{op_name}` unsupported by torch on this device/dtype") @@ -430,6 +430,101 @@ def _assert_close(actual, expected, rtol, atol): assert torch.equal(actual, expected) +def _run_max_unpool_cases(op_name, dtype, device, rtol, atol): + if op_name == "max_unpool2d": + pool = torch.nn.functional.max_pool2d + unpool = torch.nn.functional.max_unpool2d + explicit_spatial_shape = (6, 10) + explicit_kernel_size = [2, 3] + explicit_stride = [3, 4] + explicit_padding = [0, 1] + spatial_dimensions = 2 + else: + pool = torch.nn.functional.max_pool3d + unpool = torch.nn.functional.max_unpool3d + explicit_spatial_shape = (6, 10, 6) + explicit_kernel_size = [2, 3, 2] + explicit_stride = [3, 4, 3] + explicit_padding = [0, 1, 0] + spatial_dimensions = 3 + + cases = ( + ( + True, + explicit_spatial_shape, + explicit_kernel_size, + explicit_stride, + explicit_padding, + "full", + ), + ( + False, + explicit_spatial_shape, + explicit_kernel_size, + explicit_stride, + explicit_padding, + "spatial", + ), + ) + + for ( + batched, + spatial_shape, + kernel_size, + stride, + padding, + output_size_kind, + ) in cases: + input_shape = (1, 2, *spatial_shape) if batched else (2, *spatial_shape) + source = randn_strided(input_shape, None, dtype=dtype, device=device) + pooled, indices = pool( + source, + kernel_size, + stride=stride, + padding=padding, + return_indices=True, + ) + + if output_size_kind == "full": + output_size = list(source.shape) + elif output_size_kind == "spatial": + output_size = list(source.shape[-spatial_dimensions:]) + else: + output_size = None + + expected = unpool(pooled, indices, kernel_size, stride, padding, output_size) + actual = torch.empty_like(expected) + + _call_infini( + op_name, + pooled, + indices, + kernel_size, + stride, + padding, + output_size, + actual, + ) + + _assert_close(actual, expected, rtol, atol) + + if op_name == "max_unpool3d": + legacy = torch.empty_like(expected) + spatial_output_size = list(expected.shape[-spatial_dimensions:]) + resolved_stride = kernel_size if stride is None else stride + _call_infini( + op_name, + input=pooled, + indices=indices, + output_size=spatial_output_size, + stride=resolved_stride, + padding=padding, + out=legacy, + ) + + _assert_close(legacy, expected, rtol, atol) + + def _testable_ops(): """Filter the metadata down to ops the harness can drive. @@ -448,7 +543,19 @@ def _testable_ops(): seen = set() keep = [] - for op in _METADATA.get("ops", []): + ops = list(_METADATA.get("ops", [])) + + if not any(op["name"] == "max_unpool2d" for op in ops): + ops.append( + { + "name": "max_unpool2d", + "aten_name": "max_unpool2d", + "overload_name": "max_unpool2d.handwritten", + "params": [], + } + ) + + for op in ops: if op["aten_name"] in seen: continue @@ -480,6 +587,11 @@ def test_op(op_meta, shape, dtype, device, rtol, atol): _skip_if_not_active(op_name, device) _skip_low_precision_reduction(aten_name, dtype, device) + if aten_name in {"max_unpool2d", "max_unpool3d"}: + _run_max_unpool_cases(aten_name, dtype, device, rtol, atol) + + return + if aten_name in _RANDOM_OPS: pytest.skip(f"`{aten_name}` is non-deterministic (independent draws diverge)")