diff --git a/python/tvm/relax/frontend/torch/base_fx_graph_translator.py b/python/tvm/relax/frontend/torch/base_fx_graph_translator.py index b0bb14ac95ad..7552b6379a83 100644 --- a/python/tvm/relax/frontend/torch/base_fx_graph_translator.py +++ b/python/tvm/relax/frontend/torch/base_fx_graph_translator.py @@ -572,23 +572,115 @@ def call_binary_op(op, lhs, rhs): def _pow(self, node: fx.Node) -> relax.Var: lhs, rhs = self.retrieve_args(node) - # torch integer pow returns an integer tensor, but relax.op.power legalizes to - # TOPI power which requires floating-point inputs. Decompose an integer base with - # a constant non-negative integer exponent into repeated multiplication instead. - if ( - isinstance(lhs, relax.Expr) - and isinstance(lhs.ty, relax.TensorType) - and lhs.ty.dtype.matches_code(DataTypeCode.INT, DataTypeCode.UINT) - and isinstance(rhs, int) - and not isinstance(rhs, bool) - and rhs >= 0 - ): - if rhs == 0: - return self.block_builder.emit(relax.op.ones_like(lhs)) - result = lhs - for _ in range(rhs - 1): - result = self.block_builder.emit(relax.op.multiply(result, lhs)) - return result + if isinstance(lhs, relax.Expr) and isinstance(lhs.ty, relax.TensorType): + lhs_dtype = lhs.ty.dtype + is_integer_base = lhs_dtype.matches_code(DataTypeCode.INT, DataTypeCode.UINT) + is_float_base = lhs_dtype.matches_code(DataTypeCode.FLOAT, DataTypeCode.BFLOAT) + + # A Python float promotes an integer tensor to PyTorch's default floating-point + # dtype. ExportedProgram records the inferred dtype, while plain FX does not. + if is_integer_base and isinstance(rhs, float): + output_meta = node.meta.get("val") + output_dtype = self._convert_data_type( + output_meta.dtype + if isinstance(output_meta, self.torch.Tensor) + else self.torch.get_default_dtype() + ) + lhs = self.block_builder.emit(relax.op.astype(lhs, output_dtype)) + lhs_dtype = lhs.ty.dtype + is_integer_base = False + is_float_base = True + + # Match the scalar conversion used by PyTorch's floating-point power kernels. + exponent_dtype = { + "float16": self.torch.float16, + "bfloat16": self.torch.bfloat16, + "float32": self.torch.float64, + "float64": self.torch.float64, + }.get(str(lhs_dtype)) + if ( + is_float_base + and exponent_dtype is not None + and isinstance(rhs, int | float) + and not isinstance(rhs, bool) + ): + rhs = self.torch.scalar_tensor(rhs, dtype=exponent_dtype, device="cpu").item() + + is_integral_exponent = isinstance(rhs, int) or ( + isinstance(rhs, float) and rhs.is_integer() + ) + is_nonnegative_integral_exponent = is_integral_exponent and rhs >= 0 + + # TOPI power requires floating-point inputs, so decompose integer powers into + # multiplication. Exponentiation by squaring avoids linear graph growth. + if is_integer_base and is_nonnegative_integral_exponent: + exponent = int(rhs) + if exponent == 0: + return self.block_builder.emit(relax.op.ones_like(lhs)) + + result = None + factor = lhs + while exponent: + if exponent & 1: + result = ( + factor + if result is None + else self.block_builder.emit(relax.op.multiply(result, factor)) + ) + exponent >>= 1 + if exponent: + factor = self.block_builder.emit(relax.op.multiply(factor, factor)) + return result + + # Keep floating-point powers on the power kernel so low-precision inputs use + # its effective intermediate precision. Some backends do not define power for + # negative bases, even with an integral exponent, so compute the magnitude from + # abs(lhs) and restore the sign for odd exponents. Passing zero through from lhs + # also preserves the sign of negative zero. + if is_float_base and is_integral_exponent: + exponent = int(rhs) + if exponent == 0: + return self.block_builder.emit(relax.op.ones_like(lhs)) + if exponent == 1: + return lhs + + zero = relax.const(0, lhs_dtype) + magnitude_input = self.block_builder.emit(relax.op.abs(lhs)) + power_dtype = "float32" if str(lhs_dtype) in ("float16", "bfloat16") else lhs_dtype + if power_dtype != lhs_dtype: + magnitude_input = self.block_builder.emit( + relax.op.astype(magnitude_input, power_dtype) + ) + magnitude = self.block_builder.emit( + relax.op.power(magnitude_input, relax.const(rhs, power_dtype)) + ) + if power_dtype != lhs_dtype: + magnitude = self.block_builder.emit(relax.op.astype(magnitude, lhs_dtype)) + if exponent % 2 == 0: + return magnitude + + signed_magnitude = self.block_builder.emit( + relax.op.where( + self.block_builder.emit(relax.op.less(lhs, zero)), + self.block_builder.emit(relax.op.negative(magnitude)), + magnitude, + ) + ) + zero_result = lhs + if exponent < 0: + zero_result = self.block_builder.emit( + relax.op.divide(self.block_builder.emit(relax.op.ones_like(lhs)), lhs) + ) + return self.block_builder.emit( + relax.op.where( + self.block_builder.emit(relax.op.equal(lhs, zero)), + zero_result, + signed_magnitude, + ) + ) + + if is_float_base and isinstance(rhs, float): + return self.block_builder.emit(relax.op.power(lhs, relax.const(rhs, lhs_dtype))) return self._binary_op(relax.op.power, operator.pow)(node) def _div(self, node: fx.Node) -> relax.Var: diff --git a/tests/python/relax/test_frontend_from_exported_program.py b/tests/python/relax/test_frontend_from_exported_program.py index e9d2ac8b704f..50e5dd7da6f4 100644 --- a/tests/python/relax/test_frontend_from_exported_program.py +++ b/tests/python/relax/test_frontend_from_exported_program.py @@ -770,10 +770,11 @@ def main(input: R.Tensor((1, 3, 10, 10), dtype="float32")) -> R.Tuple( R.Tensor((1, 3, 10, 10), dtype="float32") ): with R.dataflow(): - lv: R.Tensor((1, 3, 10, 10), dtype="float32") = R.power( - input, R.const(2.0, "float32") + lv: R.Tensor((1, 3, 10, 10), dtype="float32") = R.abs(input) + lv1: R.Tensor((1, 3, 10, 10), dtype="float32") = R.power( + lv, R.const(2.0, "float32") ) - gv: R.Tuple(R.Tensor((1, 3, 10, 10), dtype="float32")) = (lv,) + gv: R.Tuple(R.Tensor((1, 3, 10, 10), dtype="float32")) = (lv1,) R.output(gv) return gv @@ -1079,9 +1080,8 @@ def main(input: R.Tensor((4,), dtype="int64")) -> R.Tuple(R.Tensor((4,), dtype=" # block 0 with R.dataflow(): lv: R.Tensor((4,), dtype="int64") = R.multiply(input, input) - lv1: R.Tensor((4,), dtype="int64") = R.multiply(lv, input) - lv2: R.Tensor((4,), dtype="int64") = R.multiply(lv1, input) - gv: R.Tuple(R.Tensor((4,), dtype="int64")) = (lv2,) + lv1: R.Tensor((4,), dtype="int64") = R.multiply(lv, lv) + gv: R.Tuple(R.Tensor((4,), dtype="int64")) = (lv1,) R.output(gv) return gv @@ -1089,6 +1089,186 @@ def main(input: R.Tensor((4,), dtype="int64")) -> R.Tuple(R.Tensor((4,), dtype=" verify_model(Pow(), example_args, {}, expected) +@pytest.mark.parametrize("exponent", [3, 3.0]) +def test_pow_float_integer_exponent(exponent): + class Pow(Module): + def forward(self, input): + return input.pow(exponent) + + @tvm.script.ir_module + class expected: + @R.function + def main( + input: R.Tensor((4,), dtype="float32"), + ) -> R.Tuple(R.Tensor((4,), dtype="float32")): + with R.dataflow(): + lv: R.Tensor((4,), dtype="float32") = R.abs(input) + lv1: R.Tensor((4,), dtype="float32") = R.power(lv, R.const(3.0, "float32")) + lv2: R.Tensor((4,), dtype="bool") = R.less(input, R.const(0, "float32")) + lv3: R.Tensor((4,), dtype="float32") = R.negative(lv1) + lv4: R.Tensor((4,), dtype="float32") = R.where(lv2, lv3, lv1) + lv5: R.Tensor((4,), dtype="bool") = R.equal(input, R.const(0, "float32")) + lv6: R.Tensor((4,), dtype="float32") = R.where(lv5, input, lv4) + gv: R.Tuple(R.Tensor((4,), dtype="float32")) = (lv6,) + R.output(gv) + return gv + + example_args = (torch.tensor([-2.0, -1.0, 1.0, 2.0], dtype=torch.float32),) + verify_model(Pow(), example_args, {}, expected) + verify_model_numerically(Pow(), example_args) + + +@pytest.mark.parametrize("exponent", [0, 0.0, 1, 1.0]) +def test_pow_float_integer_exponent_identity_cases(exponent): + class Pow(Module): + def forward(self, input): + return input.pow(exponent) + + example_args = (torch.tensor([-2.0, -1.0, 1.0, 2.0], dtype=torch.float32),) + verify_model_numerically(Pow(), example_args) + + +def test_pow_float_integer_exponent_large(): + class Pow(Module): + def forward(self, input): + return input.pow(17.0) + + @tvm.script.ir_module + class expected: + @R.function + def main(input: R.Tensor((2,), dtype="float32")) -> R.Tuple( + R.Tensor((2,), dtype="float32") + ): + with R.dataflow(): + lv: R.Tensor((2,), dtype="float32") = R.abs(input) + lv1: R.Tensor((2,), dtype="float32") = R.power(lv, R.const(17.0, "float32")) + lv2: R.Tensor((2,), dtype="bool") = R.less(input, R.const(0, "float32")) + lv3: R.Tensor((2,), dtype="float32") = R.negative(lv1) + lv4: R.Tensor((2,), dtype="float32") = R.where(lv2, lv3, lv1) + lv5: R.Tensor((2,), dtype="bool") = R.equal(input, R.const(0, "float32")) + lv6: R.Tensor((2,), dtype="float32") = R.where(lv5, input, lv4) + gv: R.Tuple(R.Tensor((2,), dtype="float32")) = (lv6,) + R.output(gv) + return gv + + example_args = (torch.tensor([-1.25, 0.5], dtype=torch.float32),) + verify_model(Pow(), example_args, {}, expected) + verify_model_numerically(Pow(), example_args, rtol=1e-6, atol=1e-6) + + +@pytest.mark.parametrize("exponent", [-3, -3.0]) +def test_pow_float_negative_integer_exponent(exponent): + class Pow(Module): + def forward(self, input): + return input.pow(exponent) + + example_args = (torch.tensor([-2.0, -0.0, 0.0, 2.0], dtype=torch.float32),) + verify_model_numerically(Pow(), example_args) + + +def test_pow_integer_base_float_exponent(): + class Pow(Module): + def forward(self, input): + return input.pow(3.0) + + @tvm.script.ir_module + class expected: + @R.function + def main(input: R.Tensor((4,), dtype="int32")) -> R.Tuple(R.Tensor((4,), dtype="float32")): + with R.dataflow(): + lv: R.Tensor((4,), dtype="float32") = R.astype(input, dtype="float32") + lv1: R.Tensor((4,), dtype="float32") = R.abs(lv) + lv2: R.Tensor((4,), dtype="float32") = R.power(lv1, R.const(3.0, "float32")) + lv3: R.Tensor((4,), dtype="bool") = R.less(lv, R.const(0, "float32")) + lv4: R.Tensor((4,), dtype="float32") = R.negative(lv2) + lv5: R.Tensor((4,), dtype="float32") = R.where(lv3, lv4, lv2) + lv6: R.Tensor((4,), dtype="bool") = R.equal(lv, R.const(0, "float32")) + lv7: R.Tensor((4,), dtype="float32") = R.where(lv6, lv, lv5) + gv: R.Tuple(R.Tensor((4,), dtype="float32")) = (lv7,) + R.output(gv) + return gv + + example_args = (torch.tensor([-2, -1, 1, 2], dtype=torch.int32),) + verify_model(Pow(), example_args, {}, expected) + verify_model_numerically(Pow(), example_args) + + +def test_pow_integer_base_fractional_exponent(): + class Pow(Module): + def forward(self, input): + return input.pow(0.5) + + @tvm.script.ir_module + class expected: + @R.function + def main(input: R.Tensor((4,), dtype="int32")) -> R.Tuple(R.Tensor((4,), dtype="float32")): + with R.dataflow(): + lv: R.Tensor((4,), dtype="float32") = R.astype(input, dtype="float32") + lv1: R.Tensor((4,), dtype="float32") = R.power(lv, R.const(0.5, "float32")) + gv: R.Tuple(R.Tensor((4,), dtype="float32")) = (lv1,) + R.output(gv) + return gv + + example_args = (torch.tensor([1, 4, 9, 16], dtype=torch.int32),) + verify_model(Pow(), example_args, {}, expected) + verify_model_numerically(Pow(), example_args) + + +@pytest.mark.parametrize( + "dtype, values, exponent", + [ + (torch.float16, [0.361, -0.361], 17), + (torch.bfloat16, [1.1, -1.1], 100), + (torch.float16, [-1.0], 2049.0), + (torch.bfloat16, [-1.0], 257.0), + (torch.float32, [-1.0], 2**53 + 1), + (torch.float64, [-1.0], 2**53 + 1), + ], +) +def test_pow_float_exponent_rounding(dtype, values, exponent): + class Pow(Module): + def forward(self, input): + return input.pow(exponent) + + input = torch.tensor(values, dtype=dtype) + expected = Pow()(input) + mod = from_exported_program(export(Pow(), args=(input,))) + vm = relax.VirtualMachine(relax.build(mod, "llvm"), tvm.cpu()) + actual = torch.from_dlpack(vm["main"](tvm.runtime.from_dlpack(input))[0]) + + torch.testing.assert_close(actual, expected, rtol=0, atol=0) + + +def test_pow_float_odd_exponent_preserves_signed_zero(): + class Pow(Module): + def forward(self, input): + return input.pow(3) + + input = torch.tensor([-0.0, 0.0], dtype=torch.float32) + mod = from_exported_program(export(Pow(), args=(input,))) + vm = relax.VirtualMachine(relax.build(mod, "llvm"), tvm.cpu()) + actual = torch.from_dlpack(vm["main"](tvm.runtime.from_dlpack(input))[0]) + + torch.testing.assert_close(actual, input, rtol=0, atol=0) + torch.testing.assert_close(torch.signbit(actual), torch.signbit(input)) + + +@pytest.mark.parametrize("dtype", [torch.int32, torch.bfloat16]) +@pytest.mark.parametrize("exponent", [False, True]) +def test_pow_boolean_exponent(dtype, exponent): + class Pow(Module): + def forward(self, input): + return input.pow(exponent) + + input = torch.tensor([-2, 0, 2], dtype=dtype) + expected = Pow()(input) + mod = from_exported_program(export(Pow(), args=(input,))) + vm = relax.VirtualMachine(relax.build(mod, "llvm"), tvm.cpu()) + actual = torch.from_dlpack(vm["main"](tvm.runtime.from_dlpack(input))[0]) + + torch.testing.assert_close(actual, expected) + + def test_logsoftmax(): class LogSoftmax(Module): def forward(self, input): @@ -1336,17 +1516,36 @@ def __init__(self, op): def forward(self, lhs): return self.op(lhs, 1.0) - @tvm.script.ir_module - class expected_binary2: - @R.function - def main( - lhs: R.Tensor((10, 10), dtype="float32"), - ) -> R.Tuple(R.Tensor((10, 10), dtype="float32")): - with R.dataflow(): - lv: R.Tensor((10, 10), dtype="float32") = relax_op(lhs, R.const(1.0)) - gv: R.Tuple(R.Tensor((10, 10), dtype="float32")) = (lv,) - R.output(gv) - return gv + if op is operator.pow: + + @tvm.script.ir_module + class expected_power: + @R.function + def main( + lhs: R.Tensor((10, 10), dtype="float32"), + ) -> R.Tuple(R.Tensor((10, 10), dtype="float32")): + with R.dataflow(): + gv: R.Tuple(R.Tensor((10, 10), dtype="float32")) = (lhs,) + R.output(gv) + return gv + + expected_binary2 = expected_power + + else: + + @tvm.script.ir_module + class expected_other_binary: + @R.function + def main( + lhs: R.Tensor((10, 10), dtype="float32"), + ) -> R.Tuple(R.Tensor((10, 10), dtype="float32")): + with R.dataflow(): + lv: R.Tensor((10, 10), dtype="float32") = relax_op(lhs, R.const(1.0)) + gv: R.Tuple(R.Tensor((10, 10), dtype="float32")) = (lv,) + R.output(gv) + return gv + + expected_binary2 = expected_other_binary # In-place ops (add_, mul_, ...) produce the same Relax program as their # functional counterparts: mutation outputs are dropped by the importer. diff --git a/tests/python/relax/test_frontend_from_fx.py b/tests/python/relax/test_frontend_from_fx.py index a489977958c7..480fa82e2747 100644 --- a/tests/python/relax/test_frontend_from_fx.py +++ b/tests/python/relax/test_frontend_from_fx.py @@ -2241,17 +2241,36 @@ def __init__(self, op): def forward(self, lhs): return self.op(lhs, 1.0) - @tvm.script.ir_module - class expected_binary2: - @R.function - def main( - lhs: R.Tensor((1, 3, 10, 10), dtype="float32"), - ) -> R.Tensor((1, 3, 10, 10), dtype="float32"): - with R.dataflow(): - lv: R.Tensor((1, 3, 10, 10), dtype="float32") = relax_op(lhs, R.const(1.0)) - gv: R.Tensor((1, 3, 10, 10), dtype="float32") = lv - R.output(gv) - return gv + if op is operator.pow: + + @tvm.script.ir_module + class expected_power: + @R.function + def main( + lhs: R.Tensor((1, 3, 10, 10), dtype="float32"), + ) -> R.Tensor((1, 3, 10, 10), dtype="float32"): + with R.dataflow(): + gv: R.Tensor((1, 3, 10, 10), dtype="float32") = lhs + R.output(gv) + return gv + + expected_binary2 = expected_power + + else: + + @tvm.script.ir_module + class expected_other_binary: + @R.function + def main( + lhs: R.Tensor((1, 3, 10, 10), dtype="float32"), + ) -> R.Tensor((1, 3, 10, 10), dtype="float32"): + with R.dataflow(): + lv: R.Tensor((1, 3, 10, 10), dtype="float32") = relax_op(lhs, R.const(1.0)) + gv: R.Tensor((1, 3, 10, 10), dtype="float32") = lv + R.output(gv) + return gv + + expected_binary2 = expected_other_binary verify_model(Binary1(op), input_info1, {}, expected_binary1) verify_model(Binary2(op), input_info2, {}, expected_binary2) @@ -3578,9 +3597,83 @@ class expected: def main(inp_0: R.Tensor((4,), dtype="int64")) -> R.Tensor((4,), dtype="int64"): with R.dataflow(): lv: R.Tensor((4,), dtype="int64") = R.multiply(inp_0, inp_0) - lv1: R.Tensor((4,), dtype="int64") = R.multiply(lv, inp_0) - lv2: R.Tensor((4,), dtype="int64") = R.multiply(lv1, inp_0) - gv: R.Tensor((4,), dtype="int64") = lv2 + lv1: R.Tensor((4,), dtype="int64") = R.multiply(lv, lv) + gv: R.Tensor((4,), dtype="int64") = lv1 + R.output(gv) + return gv + + verify_model(Pow(), input_info, {}, expected) + + +@pytest.mark.parametrize("exponent", [3, 3.0]) +def test_pow_float_integer_exponent(exponent): + input_info = [([4], "float32")] + + class Pow(Module): + def forward(self, input): + return input.pow(exponent) + + @tvm.script.ir_module + class expected: + @R.function + def main(inp_0: R.Tensor((4,), dtype="float32")) -> R.Tensor((4,), dtype="float32"): + with R.dataflow(): + lv: R.Tensor((4,), dtype="float32") = R.abs(inp_0) + lv1: R.Tensor((4,), dtype="float32") = R.power(lv, R.const(3.0, "float32")) + lv2: R.Tensor((4,), dtype="bool") = R.less(inp_0, R.const(0, "float32")) + lv3: R.Tensor((4,), dtype="float32") = R.negative(lv1) + lv4: R.Tensor((4,), dtype="float32") = R.where(lv2, lv3, lv1) + lv5: R.Tensor((4,), dtype="bool") = R.equal(inp_0, R.const(0, "float32")) + lv6: R.Tensor((4,), dtype="float32") = R.where(lv5, inp_0, lv4) + gv: R.Tensor((4,), dtype="float32") = lv6 + R.output(gv) + return gv + + verify_model(Pow(), input_info, {}, expected) + + +def test_pow_integer_base_float_exponent(): + input_info = [([4], "int32")] + + class Pow(Module): + def forward(self, input): + return input.pow(3.0) + + @tvm.script.ir_module + class expected: + @R.function + def main(inp_0: R.Tensor((4,), dtype="int32")) -> R.Tensor((4,), dtype="float32"): + with R.dataflow(): + lv: R.Tensor((4,), dtype="float32") = R.astype(inp_0, dtype="float32") + lv1: R.Tensor((4,), dtype="float32") = R.abs(lv) + lv2: R.Tensor((4,), dtype="float32") = R.power(lv1, R.const(3.0, "float32")) + lv3: R.Tensor((4,), dtype="bool") = R.less(lv, R.const(0, "float32")) + lv4: R.Tensor((4,), dtype="float32") = R.negative(lv2) + lv5: R.Tensor((4,), dtype="float32") = R.where(lv3, lv4, lv2) + lv6: R.Tensor((4,), dtype="bool") = R.equal(lv, R.const(0, "float32")) + lv7: R.Tensor((4,), dtype="float32") = R.where(lv6, lv, lv5) + gv: R.Tensor((4,), dtype="float32") = lv7 + R.output(gv) + return gv + + verify_model(Pow(), input_info, {}, expected) + + +def test_pow_integer_base_fractional_exponent(): + input_info = [([4], "int32")] + + class Pow(Module): + def forward(self, input): + return input.pow(0.5) + + @tvm.script.ir_module + class expected: + @R.function + def main(inp_0: R.Tensor((4,), dtype="int32")) -> R.Tensor((4,), dtype="float32"): + with R.dataflow(): + lv: R.Tensor((4,), dtype="float32") = R.astype(inp_0, dtype="float32") + lv1: R.Tensor((4,), dtype="float32") = R.power(lv, R.const(0.5, "float32")) + gv: R.Tensor((4,), dtype="float32") = lv1 R.output(gv) return gv