This is an automated email from the ASF dual-hosted git repository.
tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git
The following commit(s) were added to refs/heads/main by this push:
new 591cf1ec42 [Relax] Remove segfault in R.call_tir_inplace validation
(#17242)
591cf1ec42 is described below
commit 591cf1ec4281872b97449fdd0da56ff255c9f383
Author: Eric Lunderberg <[email protected]>
AuthorDate: Tue Aug 6 07:03:37 2024 -0500
[Relax] Remove segfault in R.call_tir_inplace validation (#17242)
Prior to this commit, the error message produced when validating
`R.call_tir_inplace` included the shape of the argument that will be
mutated in-place. This correctly caught and raised an error when the
argument is a
tensor with known shape that is incompatible with the output tensor's
shape. However, this same error message could be also be reached if
the input does not have `TensorStructInfo` at all, which would trigger
a segfault.
This commit updates the validation to print the argument's
`StructInfo` directly, rather than a field from the struct info. This
correctly raises an error for the cases where the argument is not a
tensor, or is a tensor with unknown dimensionality, while still
printing the explicit shape of the mismatched tensor when avalable.
---
src/relax/op/op.cc | 80 +++++++-------
tests/python/relax/test_transform.py | 197 +++++++++++++++++++++++++++++------
2 files changed, 202 insertions(+), 75 deletions(-)
diff --git a/src/relax/op/op.cc b/src/relax/op/op.cc
index 77cf4a2c6f..0a840248ff 100644
--- a/src/relax/op/op.cc
+++ b/src/relax/op/op.cc
@@ -419,13 +419,19 @@ Expr NormalizeCallTIRInPlace(const BlockBuilder& ctx,
Call call) {
// may result in an error if performed before normalization.
call = Downcast<Call>(NormalizeCallTIR(ctx, std::move(call)));
+ Array<StructInfo> sinfo_outputs = [&]() -> Array<StructInfo> {
+ auto out_sinfo = call->sinfo_args[0];
+ if (auto* tuple_output = out_sinfo.as<TupleStructInfoNode>()) {
+ return tuple_output->fields;
+ } else {
+ return {out_sinfo};
+ }
+ }();
+
// there must be an inplace index for each output
const auto* attrs = call->attrs.as<CallTIRInplaceAttrs>();
- size_t num_outputs = 1U;
- if (auto* tup_info = call->sinfo_args[0].as<TupleStructInfoNode>()) {
- num_outputs = tup_info->fields.size();
- }
- if (attrs->inplace_indices.size() != num_outputs) {
+ ICHECK(attrs);
+ if (attrs->inplace_indices.size() != sinfo_outputs.size()) {
ctx->ReportFatal(Diagnostic::Error(call)
<< "There must be an in-place index specified for each
output");
}
@@ -459,45 +465,37 @@ Expr NormalizeCallTIRInPlace(const BlockBuilder& ctx,
Call call) {
// input shape
// TODO(@slyubomirsky): eventually we will want to handle cases where that
is not true
Tuple call_args = Downcast<Tuple>(call->args[1]);
- if (attrs->inplace_indices.size() == 1) {
- auto* out_sinfo = call->sinfo_args[0].as<TensorStructInfoNode>();
- if (!out_sinfo) {
- ctx->ReportFatal(Diagnostic::Error(call) << "The output struct info must
be a tensor");
+
+ for (size_t i_output = 0; i_output < attrs->inplace_indices.size();
i_output++) {
+ auto i_input = attrs->inplace_indices[i_output].IntValue();
+ if (i_input == -1) {
+ continue;
}
- auto* input_sinfo = GetStructInfoAs<TensorStructInfoNode>(
- call_args->fields[attrs->inplace_indices[0].IntValue()]);
- if (!input_sinfo || !input_sinfo->shape.defined() ||
- !CanProveShapeEqual(input_sinfo->shape.value(),
out_sinfo->shape.value(),
- ctx->GetAnalyzer())) {
+
+ auto sinfo_output = sinfo_outputs[i_output];
+ auto tinfo_output = sinfo_output.as<TensorStructInfoNode>();
+
+ if (!tinfo_output || !tinfo_output->shape.defined() ||
tinfo_output->IsUnknownDtype()) {
ctx->ReportFatal(Diagnostic::Error(call)
- << "The shape of output 0 must match input "
- << attrs->inplace_indices[0].IntValue() << ", whereas
we have "
- << out_sinfo->shape.value() << " in output 0 versus "
- << input_sinfo->shape.value() << " in input "
- << attrs->inplace_indices[0].IntValue());
+ << "The output struct info for an in-place mutation
must be a tensor "
+ << "with a defined shape and dtype, "
+ << "but output " << i_output << " has struct info " <<
sinfo_output);
}
- } else {
- auto out_sinfos = call->sinfo_args[0].as<TupleStructInfoNode>()->fields;
- for (size_t i = 0; i < attrs->inplace_indices.size(); i++) {
- if (attrs->inplace_indices[i].IntValue() == -1) {
- continue;
- }
- auto* out_sinfo = out_sinfos[i].as<TensorStructInfoNode>();
- if (!out_sinfo) {
- ctx->ReportFatal(Diagnostic::Error(call) << "The output struct info
must be a tensor");
- }
- auto* input_sinfo = GetStructInfoAs<TensorStructInfoNode>(
- call_args->fields[attrs->inplace_indices[i].IntValue()]);
- if (!input_sinfo || !input_sinfo->shape.defined() ||
- !CanProveShapeEqual(input_sinfo->shape.value(),
out_sinfo->shape.value(),
- ctx->GetAnalyzer())) {
- ctx->ReportFatal(Diagnostic::Error(call)
- << "The shape of output " << i << " must match that
of input "
- << attrs->inplace_indices[i].IntValue() << ", whereas
we have "
- << out_sinfo->shape.value() << " in output " << i <<
" versus "
- << input_sinfo->shape.value() << " in input "
- << attrs->inplace_indices[i].IntValue());
- }
+
+ auto sinfo_input = GetStructInfo(call_args->fields[i_input]);
+ auto tinfo_input = sinfo_input.as<TensorStructInfoNode>();
+
+ if (!tinfo_input ||
+ (tinfo_output->IsUnknownDtype() || tinfo_output->dtype !=
tinfo_input->dtype) ||
+ (!tinfo_input->shape.defined() ||
+ !CanProveShapeEqual(tinfo_input->shape.value(),
tinfo_output->shape.value(),
+ ctx->GetAnalyzer()))) {
+ ctx->ReportFatal(Diagnostic::Error(call)
+ << "The input used for an in-place mutation must be "
+ << "a tensor with identical shape and dtype as the
output. "
+ << "However, output " << i_output << " with struct info
" << sinfo_output
+ << " is specified as an in-place mutation of input " <<
i_input
+ << " with struct info " << sinfo_input);
}
}
diff --git a/tests/python/relax/test_transform.py
b/tests/python/relax/test_transform.py
index e7e8f94fc2..ee2df866fb 100644
--- a/tests/python/relax/test_transform.py
+++ b/tests/python/relax/test_transform.py
@@ -20,7 +20,7 @@ import tvm
from tvm import relax
import tvm.script
-from tvm.script import tir as T, relax as R
+from tvm.script import ir as I, tir as T, relax as R
def test_to_non_dataflow():
@@ -446,45 +446,174 @@ def test_call_tir_inplace_some_new():
tvm.ir.assert_structural_equal(Expected["foo"], new_mod["foo"],
map_free_vars=True)
[email protected]()
def test_call_tir_inplace_repeated_input():
- @tvm.script.ir_module
- class Input:
- @T.prim_func
- def func(
- A: T.Buffer((2, 3), "int32"), B: T.Buffer((2, 3), "int32"), C:
T.Buffer((2, 3), "int32")
- ):
- T.evaluate(0)
+ with pytest.raises(tvm.error.DiagnosticError):
+
+ @tvm.script.ir_module
+ class Input:
+ @T.prim_func
+ def func(
+ A: T.Buffer((2, 3), "int32"),
+ B: T.Buffer((2, 3), "int32"),
+ C: T.Buffer((2, 3), "int32"),
+ ):
+ T.evaluate(0)
- @R.function
- def foo(
- x: R.Tensor((2, 3), "int32"), y: R.Tensor((2, 3), "int32"), z:
R.Tensor((2, 3), "int32")
- ) -> R.Tuple(R.Tensor((2, 3), "int32"), R.Tensor((2, 3), "int32")):
- R.func_attr({"relax.force_pure": True})
- gv0 = R.call_tir_inplace(
- Input.func,
- (x, y, z),
- # repeated 0 -> that's an error
- [0, 0],
- [R.Tensor((2, 3), dtype="int32"), R.Tensor((2, 3),
dtype="int32")],
- )
- return gv0
+ @R.function
+ def foo(
+ x: R.Tensor((2, 3), "int32"),
+ y: R.Tensor((2, 3), "int32"),
+ z: R.Tensor((2, 3), "int32"),
+ ) -> R.Tuple(R.Tensor((2, 3), "int32"), R.Tensor((2, 3), "int32")):
+ R.func_attr({"relax.force_pure": True})
+ gv0 = R.call_tir_inplace(
+ Input.func,
+ (x, y, z),
+ # repeated 0 -> that's an error
+ [0, 0],
+ [R.Tensor((2, 3), dtype="int32"), R.Tensor((2, 3),
dtype="int32")],
+ )
+ return gv0
[email protected]()
def test_call_tir_inplace_all_new():
- @tvm.script.ir_module
- class Input:
- @T.prim_func
- def func(A: T.Buffer((2, 3), "int32")):
- T.evaluate(0)
+ with pytest.raises(tvm.error.DiagnosticError):
- @R.function
- def foo(x: R.Tensor((2, 3), "int32")) -> R.Tensor((2, 3), "int32"):
- R.func_attr({"relax.force_pure": True})
- # cannot make the only output a fresh one
- gv0 = R.call_tir_inplace(Input.func, x, -1, R.Tensor((2, 3),
dtype="int32"))
- return gv0
+ @tvm.script.ir_module
+ class Input:
+ @T.prim_func
+ def func(A: T.Buffer((2, 3), "int32")):
+ T.evaluate(0)
+
+ @R.function
+ def foo(x: R.Tensor((2, 3), "int32")) -> R.Tensor((2, 3), "int32"):
+ R.func_attr({"relax.force_pure": True})
+ # cannot make the only output a fresh one
+ gv0 = R.call_tir_inplace(Input.func, x, -1, R.Tensor((2, 3),
dtype="int32"))
+ return gv0
+
+
+def test_inplace_mutation_with_tuple_argument_raises_error():
+ """TIR PrimFuncs do not support Tuple arguments
+
+ The `R.call_tir_inplace` operator must receive an in-line tuple of
+ arguments, where each argument in the tuple may be expressed in
+ TIR. Here, `[[A]]` specifies a tuple of arguments, where the
+ first argument is itself a tuple. Since PrimFuncs do not support
+ Tuple arguments, this is invalid.
+
+ This is a regression test. In previous implementations, this
+ triggered a segfault rather than raising an exception.
+
+ """
+ with pytest.raises(tvm.error.DiagnosticError):
+
+ @I.ir_module
+ class Module:
+ @R.function
+ def main(A: R.Tensor((16,), dtype="float32")) -> R.Tensor((16,),
dtype="float32"):
+ cls = Module
+ gv1 = R.call_tir_inplace(
+ cls.multiply_by_two,
+ [[A]],
+ out_sinfo=R.Tensor((16,), dtype="float32"),
+ inplace_indices=[0],
+ )
+ return gv1
+
+ @T.prim_func(private=True)
+ def multiply_by_two(A: T.Buffer((16,), "float32")):
+ for i in range(16):
+ A[i] = A[i] * T.float32(2)
+
+
+def test_inplace_mutation_with_non_tensor_argument_raises_error():
+ """In-place argument must be a tensor
+
+ The `R.call_tir_inplace` operator must receive an in-line tuple of
+ arguments, where each argument in the tuple may be expressed in
+ TIR. Here, the argument `A` is not a tensor.
+
+ This is a regression test. In previous implementations, this
+ triggered a segfault rather than raising an exception.
+
+ """
+ with pytest.raises(tvm.error.DiagnosticError):
+
+ @I.ir_module
+ class Module:
+ @R.function
+ def main(A: R.Object):
+ gv1 = R.call_tir_inplace(
+ Module.multiply_by_two,
+ [A],
+ out_sinfo=R.Tensor((16,), dtype="float32"),
+ inplace_indices=[0],
+ )
+ return gv1
+
+ @T.prim_func(private=True)
+ def multiply_by_two(A: T.Buffer((16,), "float32")):
+ for i in range(16):
+ A[i] = A[i] * T.float32(2)
+
+
+def test_inplace_mutation_with_incompatible_tensor_shape_raises_error():
+ """In-place argument must have compatible shape
+
+ The `R.call_tir_inplace` operator must receive an in-line tuple of
+ arguments, where the shape of each in-place argument is compatible
+ with the corresponding output. Here, the shape of argument `A` is
+ different than the output's shape (`[32]` as opposed to `[16]`).
+
+ """
+ with pytest.raises(tvm.error.DiagnosticError):
+
+ @I.ir_module
+ class Module:
+ @R.function
+ def main(A: R.Tensor([32], dtype="float32")):
+ gv1 = R.call_tir_inplace(
+ Module.multiply_by_two,
+ [A],
+ out_sinfo=R.Tensor((16,), dtype="float32"),
+ inplace_indices=[0],
+ )
+ return gv1
+
+ @T.prim_func(private=True)
+ def multiply_by_two(A: T.Buffer((16,), "float32")):
+ for i in range(16):
+ A[i] = A[i] * T.float32(2)
+
+
+def test_inplace_mutation_with_incompatible_tensor_dtype_raises_error():
+ """In-place argument must have compatible dtype
+
+ The `R.call_tir_inplace` operator must receive an in-line tuple of
+ arguments, where the shape of each in-place argument is compatible
+ with the corresponding output. Here, the dtype of argument `A` is
+ different than the output's dtype (`int32` as opposed to `float32`).
+
+ """
+ with pytest.raises(tvm.error.DiagnosticError):
+
+ @I.ir_module
+ class Module:
+ @R.function
+ def main(A: R.Tensor([16], dtype="int32")):
+ gv1 = R.call_tir_inplace(
+ Module.multiply_by_two,
+ [A],
+ out_sinfo=R.Tensor((16,), dtype="float32"),
+ inplace_indices=[0],
+ )
+ return gv1
+
+ @T.prim_func(private=True)
+ def multiply_by_two(A: T.Buffer((16,), "float32")):
+ for i in range(16):
+ A[i] = A[i] * T.float32(2)
if __name__ == "__main__":