This is an automated email from the ASF dual-hosted git repository.
MasterJH5574 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 eafcba1c44 [Relax][TensorRT] Fix YOLO BYOC offload and partitioning
gaps (#19998)
eafcba1c44 is described below
commit eafcba1c44fffbb881bb5f838ad72e0cfafa9ce5
Author: Shushi Hong <[email protected]>
AuthorDate: Thu Jul 16 15:57:40 2026 -0400
[Relax][TensorRT] Fix YOLO BYOC offload and partitioning gaps (#19998)
Fixes #19887.
This PR fixes several Relax TensorRT BYOC issues exposed by YOLO-style
models:
- adds TensorRT support for SiLU and resize2d
- preserves operand and TupleGetItem ordering during codegen
- fixes cyclic and unsafe Tuple/TGI region merging
- handles static Shape bindings and nested packed-function outputs
- normalizes PrimType dtype arguments passed to relax.arange
With these changes, yolo11n-seg can be merged into a single TensorRT
region, while yolo11n can be imported and partitioned successfully.
---
python/tvm/relax/backend/contrib/tensorrt.py | 84 +++++++++-
python/tvm/relax/op/create.py | 6 +-
src/relax/backend/contrib/tensorrt/codegen.cc | 66 +++++---
src/relax/transform/call_tir_rewrite.cc | 138 +++++++++------
src/relax/transform/fuse_ops.cc | 142 +++++++++-------
src/relax/transform/merge_composite_functions.cc | 156 ++++++++++++++++-
src/runtime/extra/contrib/tensorrt/tensorrt_ops.cc | 99 +++++++++++
tests/python/relax/test_backend_tensorrt.py | 117 +++++++++++++
tests/python/relax/test_codegen_tensorrt.py | 121 ++++++++++++--
tests/python/relax/test_op_create.py | 5 +
tests/python/relax/test_transform.py | 44 +++++
.../relax/test_transform_fuse_ops_by_pattern.py | 76 +++++++++
.../test_transform_merge_composite_functions.py | 186 +++++++++++++++++++++
13 files changed, 1072 insertions(+), 168 deletions(-)
diff --git a/python/tvm/relax/backend/contrib/tensorrt.py
b/python/tvm/relax/backend/contrib/tensorrt.py
index 303ebc394c..21ab01b24d 100644
--- a/python/tvm/relax/backend/contrib/tensorrt.py
+++ b/python/tvm/relax/backend/contrib/tensorrt.py
@@ -23,15 +23,17 @@ converter registered under the same name (the converters
are keyed by
out of the module and annotates them for the ``tensorrt`` codegen.
"""
-from collections.abc import Mapping
-
+from tvm import tirx
from tvm.ir import IRModule
-from tvm.relax.dpl.pattern import DFPattern, is_op, wildcard
-from tvm.relax.transform import FuseOpsByPattern, MergeCompositeFunctions
-
-from ..pattern_registry import get_patterns_with_prefix, register_patterns
+from tvm.relax.dpl.pattern import is_op, wildcard
+from tvm.relax.expr import ShapeExpr
+from tvm.relax.transform import (
+ FuseOpsByPattern,
+ MergeCompositeFunctions,
+ PatternCheckContext,
+)
-Pattern = tuple[str, DFPattern, Mapping[str, DFPattern]]
+from ..pattern_registry import Pattern, get_patterns_with_prefix,
register_patterns
def _op_pattern(composite_name: str, op_name: str, num_args: int) -> Pattern:
@@ -40,6 +42,70 @@ def _op_pattern(composite_name: str, op_name: str, num_args:
int) -> Pattern:
return (composite_name, is_op(op_name)(*args), {})
+def _resize2d_pattern() -> Pattern:
+ """Match the subset of resize2d that the TensorRT converter implements
exactly."""
+
+ data = wildcard()
+ size = wildcard()
+ root = is_op("relax.image.resize2d")(data, size)
+
+ def check(context: PatternCheckContext) -> bool:
+ data_expr = context.annotated_expr["data"]
+ size_expr = context.annotated_expr["size"]
+ resize = context.annotated_expr["root"]
+
+ # Follow an ANF binding when the static ShapeExpr was assigned to a
variable.
+ while size_expr in context.matched_bindings:
+ size_expr = context.matched_bindings[size_expr]
+
+ if not isinstance(size_expr, ShapeExpr) or len(size_expr.values) != 2:
+ return False
+ if not all(isinstance(dim, tirx.IntImm) and dim.value > 0 for dim in
size_expr.values):
+ return False
+
+ attrs = resize.attrs
+ if attrs.layout != "NCHW":
+ return False
+ if data_expr.ty.ndim != 4 or data_expr.ty.shape is None:
+ return False
+
+ # The runtime's DLPack-to-TensorRT dtype mapper supports only
FP16/FP32. TensorRT Resize
+ # itself preserves its input dtype, so a distinct Relax out_dtype is
not supported here.
+ if data_expr.ty.dtype not in ("float16", "float32"):
+ return False
+ if resize.ty.dtype != data_expr.ty.dtype:
+ return False
+
+ if attrs.method not in ("nearest_neighbor", "linear", "cubic"):
+ return False
+ if attrs.coordinate_transformation_mode not in (
+ "asymmetric",
+ "align_corners",
+ "half_pixel",
+ "pytorch_half_pixel",
+ ):
+ return False
+
+ # Relax `round` uses ties-to-even, for which TensorRT has no
ResizeRoundMode. The other
+ # modes map exactly to TensorRT's FLOOR, CEIL, HALF_UP, and HALF_DOWN
modes.
+ if attrs.method == "nearest_neighbor" and attrs.rounding_method not in
(
+ "floor",
+ "ceil",
+ "round_prefer_ceil",
+ "round_prefer_floor",
+ ):
+ return False
+
+ return True
+
+ return (
+ "tensorrt.image.resize2d",
+ root,
+ {"data": data, "size": size, "root": root},
+ check,
+ )
+
+
def _tensorrt_patterns() -> list[Pattern]:
patterns: list[Pattern] = []
@@ -47,6 +113,7 @@ def _tensorrt_patterns() -> list[Pattern]:
for composite, op in [
("tensorrt.nn.relu", "relax.nn.relu"),
("tensorrt.sigmoid", "relax.sigmoid"),
+ ("tensorrt.nn.silu", "relax.nn.silu"),
("tensorrt.tanh", "relax.tanh"),
("tensorrt.exp", "relax.exp"),
("tensorrt.log", "relax.log"),
@@ -92,6 +159,9 @@ def _tensorrt_patterns() -> list[Pattern]:
]:
patterns.append(_op_pattern(composite, op, 2))
+ # image.resize2d (data + target-size shape argument).
+ patterns.append(_resize2d_pattern())
+
# Convolutions and matmul (data + weight).
for composite, op in [
("tensorrt.nn.conv1d", "relax.nn.conv1d"),
diff --git a/python/tvm/relax/op/create.py b/python/tvm/relax/op/create.py
index e8c1300f01..5188140222 100644
--- a/python/tvm/relax/op/create.py
+++ b/python/tvm/relax/op/create.py
@@ -245,7 +245,7 @@ def arange(
start: PrimExprLike,
end: PrimExprLike | None = None,
step: PrimExprLike = 1,
- dtype: str | DataType | None = None,
+ dtype: str | DataType | PrimType | None = None,
) -> Expr:
"""Construct a tensor with evenly spaced elements.
@@ -261,7 +261,7 @@ def arange(
step : PrimExprLike
The step size.
- dtype : Optional[str | DataType]
+ dtype : Optional[str | DataType | PrimType]
The data type of the created tensor.
Returns
@@ -288,7 +288,7 @@ def arange(
start = prim_value(start)
end = prim_value(end)
step = prim_value(step)
- return _ffi_api.arange(start, end, step, dtype) # type: ignore
+ return _ffi_api.arange(start, end, step, _raw_dtype(dtype)) # type: ignore
def hamming_window(window_size, periodic, alpha, beta, dtype):
diff --git a/src/relax/backend/contrib/tensorrt/codegen.cc
b/src/relax/backend/contrib/tensorrt/codegen.cc
index b09c988931..2b5d4d3db7 100644
--- a/src/relax/backend/contrib/tensorrt/codegen.cc
+++ b/src/relax/backend/contrib/tensorrt/codegen.cc
@@ -26,11 +26,13 @@
#include <tvm/ir/module.h>
#include <tvm/ir/op.h>
#include <tvm/ir/transform.h>
+#include <tvm/relax/analysis.h>
#include <tvm/relax/attrs/manipulate.h>
#include <tvm/relax/attrs/nn.h>
#include <tvm/relax/attrs/statistical.h>
#include <tvm/relax/expr.h>
#include <tvm/relax/type.h>
+#include <tvm/relax/utils.h>
#include <tvm/runtime/logging.h>
#include <tvm/tirx/index_map.h>
@@ -97,18 +99,13 @@ using JSONGraphObjectPtr =
backend::contrib::JSONGraphObjectPtr;
using OpAttrExtractor = backend::contrib::OpAttrExtractor;
using JSONSerializer = backend::contrib::JSONSerializer;
-class TensorRTJSONSerializer;
-
/*!
- * \brief Collect the constants and attributes from all operator calls in the
body
- * of a "Composite" function.
+ * \brief Collect the primitive operator call and its attributes from a
"Composite" function.
*/
class CollectFromCompositeFunctionBody : public ExprVisitor {
public:
- explicit CollectFromCompositeFunctionBody(TensorRTJSONSerializer* serializer)
- : serializer_(serializer), node_(std::make_shared<JSONGraphNode>()) {}
+ CollectFromCompositeFunctionBody() :
node_(std::make_shared<JSONGraphNode>()) {}
- void VisitExpr_(const ConstantNode* constant_node) final;
void VisitExpr_(const CallNode* call_node) final;
void SetGenericAttributes(const CallNode* call_node) {
@@ -212,9 +209,8 @@ class CollectFromCompositeFunctionBody : public ExprVisitor
{
return true;
}
- TensorRTJSONSerializer* serializer_;
- /*! \brief Accumulated translated arguments. */
- std::vector<JSONGraphNodeEntry> args_;
+ /*! \brief The primitive operator call in the composite function. */
+ const CallNode* operator_call_{nullptr};
/*!
* \brief Temporary node into which we'll accumulate attributes. Ideally
this would be the
* final JSONGraphNode however we don't yet know how many inputs that will
have.
@@ -245,20 +241,37 @@ class TensorRTJSONSerializer : public JSONSerializer {
TVM_FFI_ICHECK(opt_composite.has_value());
std::string name = opt_composite.value();
- // Collect the constants and attributes of all operator calls inside the
composite body.
- CollectFromCompositeFunctionBody collector(this);
+ // TensorRT patterns describe a single primitive operator and use the
Composite function only
+ // to bind that operator's arguments and attributes.
+ CollectFromCompositeFunctionBody collector;
collector.VisitExpr(fn->body);
-
- // Capture the args to the "Composite" function as inputs for this node.
- std::vector<JSONGraphNodeEntry> inputs;
- for (const auto& arg : call_node->args) {
- auto res = VisitExpr(arg);
- inputs.insert(inputs.end(), res.begin(), res.end());
+ TVM_FFI_ICHECK(collector.operator_call_ != nullptr)
+ << "TensorRT Composite function " << name
+ << " must contain exactly one primitive Relax operator call";
+
+ // Bind Composite parameters back to the caller. The primitive operator's
tensor arguments
+ // are serialized below in their original order, regardless of whether
they are constants or
+ // parameters. Non-tensor scalar and shape arguments have already been
captured as attributes.
+ TVM_FFI_ICHECK_EQ(fn->params.size(), call_node->args.size());
+ ffi::Map<Var, Expr> param_bindings;
+ for (size_t i = 0; i < call_node->args.size(); ++i) {
+ param_bindings.Set(fn->params[i], call_node->args[i]);
}
- // Capture constants from the composite function body as additional inputs
for this node.
- for (const auto& node : collector.args_) {
- inputs.emplace_back(node);
+ std::vector<JSONGraphNodeEntry> inputs;
+ auto append_tensor_inputs = [&](const auto& self, const Expr& expr) ->
void {
+ if (const auto* tuple = expr.as<TupleNode>()) {
+ for (const Expr& field : tuple->fields) self(self, field);
+ return;
+ }
+ Type type = GetType(expr);
+ if (type->IsInstance<TensorTypeNode>() ||
type->IsInstance<TupleTypeNode>()) {
+ auto entries = VisitExpr(expr);
+ inputs.insert(inputs.end(), entries.begin(), entries.end());
+ }
+ };
+ for (const Expr& arg : collector.operator_call_->args) {
+ append_tensor_inputs(append_tensor_inputs, Bind(arg, param_bindings));
}
// Create the final node.
@@ -299,13 +312,12 @@ class TensorRTJSONSerializer : public JSONSerializer {
ffi::Map<Var, Expr> bindings_;
};
-void CollectFromCompositeFunctionBody::VisitExpr_(const ConstantNode*
constant_node) {
- for (const auto& entry :
serializer_->VisitExpr(ffi::GetRef<Constant>(constant_node))) {
- args_.emplace_back(entry);
- }
-}
-
void CollectFromCompositeFunctionBody::VisitExpr_(const CallNode* call_node) {
+ TVM_FFI_ICHECK(call_node->op->IsInstance<OpNode>())
+ << "TensorRT Composite functions must contain exactly one primitive
Relax operator call";
+ TVM_FFI_ICHECK(operator_call_ == nullptr)
+ << "TensorRT Composite functions must contain exactly one primitive
Relax operator call";
+ operator_call_ = call_node;
if (!TrySetLayoutTransformAttributes(call_node)) {
SetGenericAttributes(call_node);
SetArgumentAttributes(call_node);
diff --git a/src/relax/transform/call_tir_rewrite.cc
b/src/relax/transform/call_tir_rewrite.cc
index e1d9a265dc..20c7027c97 100644
--- a/src/relax/transform/call_tir_rewrite.cc
+++ b/src/relax/transform/call_tir_rewrite.cc
@@ -29,6 +29,8 @@
#include <tvm/relax/type.h>
#include <tvm/tirx/op.h>
+#include <algorithm>
+
#include "utils.h"
namespace tvm {
@@ -73,27 +75,12 @@ class CallTIRMutator : public ExprMutator {
bool is_inplace = call->op.same_as(call_tir_inplace_op);
const auto* inplace_attrs = call->attrs.as<CallTIRInplaceAttrs>();
ffi::Array<Expr> outs;
+ ffi::Optional<TupleType> tuple_output_type;
if (const auto& tensor_ty = MatchType<TensorType>(expr)) {
// single output case
const TensorType& output_ty = tensor_ty.value();
- TVM_FFI_ICHECK(output_ty->shape.has_value())
- << "the TensorType shape of call_tir has not populated";
- int dev_index = 0;
- ffi::String scope = "global";
- if (output_ty->vdevice.has_value()) {
- dev_index = GetDeviceIndex(mod_, output_ty->vdevice.value());
- scope = output_ty->vdevice.value()->memory_scope;
- } else {
- dev_index = GetDeviceIndexByScope(mod_, scope);
- }
-
if (!is_inplace) {
- outs.push_back(builder_->Emit(Call(Type::Missing(), alloc_tensor_op,
-
{output_ty->shape.value().as_or_throw<ShapeExpr>(),
-
DataTypeImm(output_ty->dtype.value()->dtype),
- IntImm::Int64(dev_index),
StringImm(scope)},
- Attrs(), {output_ty}),
- "alloc"));
+ outs.push_back(AllocateOutputTensor(output_ty, alloc_tensor_op));
} else {
// if there is only one output, it must be an in-place argument, but
check anyway
TVM_FFI_ICHECK(inplace_attrs->inplace_indices[0] != -1)
@@ -105,35 +92,30 @@ class CallTIRMutator : public ExprMutator {
} else if (const auto& tuple_ty = MatchType<TupleType>(expr)) {
// multiple output case
const TupleType& output_ty = tuple_ty.value();
- for (size_t i = 0; i < output_ty->fields.size(); ++i) {
- const auto& field = output_ty->fields[i];
-
- TVM_FFI_ICHECK(field->IsInstance<TensorTypeNode>())
- << "call_tir expects Tuple of TensorType, but got " << field
- << " as an element of TupleType";
- const auto& field_tensor = field.as_or_throw<TensorType>();
- TVM_FFI_ICHECK(field_tensor->shape.has_value())
- << "call_tir expects all TensorType has shape, but got " <<
field_tensor
- << " as an element of TupleType";
-
- int dev_index = 0;
- ffi::String scope = "global";
- if (field_tensor->vdevice.has_value()) {
- dev_index = GetDeviceIndex(mod_, field_tensor->vdevice.value());
- scope = field_tensor->vdevice.value()->memory_scope;
- }
-
- if (!is_inplace || inplace_attrs->inplace_indices[i] == -1) {
- outs.push_back(
- builder_->Emit(Call(Type::Missing(), alloc_tensor_op,
-
{field_tensor->shape.value().as_or_throw<ShapeExpr>(),
-
DataTypeImm(field_tensor->dtype.value()->dtype),
- IntImm::Int64(dev_index),
StringImm(scope)},
- Attrs(), {field_tensor}),
- "alloc"));
- } else {
- outs.push_back(
-
call->args[1].as_or_throw<Tuple>()->fields[inplace_attrs->inplace_indices[i]]);
+ tuple_output_type = output_ty;
+ bool has_nested_tuple =
+ std::any_of(output_ty->fields.begin(), output_ty->fields.end(),
+ [](const Type& field) { return
field->IsInstance<TupleTypeNode>(); });
+
+ if (has_nested_tuple) {
+ TVM_FFI_ICHECK(!is_inplace)
+ << "call_tir_inplace does not support nested tuple output types";
+ FlattenAndAllocateOutputs(output_ty, alloc_tensor_op, &outs);
+ } else {
+ for (size_t i = 0; i < output_ty->fields.size(); ++i) {
+ const auto& field = output_ty->fields[i];
+
+ TVM_FFI_ICHECK(field->IsInstance<TensorTypeNode>())
+ << "call_tir expects Tuple of TensorType, but got " << field
+ << " as an element of TupleType";
+ const auto& field_tensor = field.as_or_throw<TensorType>();
+
+ if (!is_inplace || inplace_attrs->inplace_indices[i] == -1) {
+ outs.push_back(AllocateOutputTensor(field_tensor,
alloc_tensor_op));
+ } else {
+ outs.push_back(
+
call->args[1].as_or_throw<Tuple>()->fields[inplace_attrs->inplace_indices[i]]);
+ }
}
}
} else {
@@ -166,15 +148,75 @@ class CallTIRMutator : public ExprMutator {
builder_->Emit(Call(Type::Missing(), call->args[0], args), "_");
}
- if (outs.size() == 1) {
- return outs[0];
+ if (tuple_output_type.has_value()) {
+ size_t index = 0;
+ Expr output = RebuildOutputTuple(tuple_output_type.value(), outs,
&index);
+ TVM_FFI_ICHECK_EQ(index, outs.size());
+ return output;
}
+ if (outs.size() == 1) return outs[0];
return std::move(Tuple(outs));
}
return ffi::GetRef<Expr>(call);
}
+ Expr AllocateOutputTensor(const TensorType& tensor_ty, const Op&
alloc_tensor_op) {
+ TVM_FFI_ICHECK(tensor_ty->shape.has_value())
+ << "call_tir expects all TensorType has shape, but got " << tensor_ty;
+
+ int dev_index = 0;
+ ffi::String scope = "global";
+ if (tensor_ty->vdevice.has_value()) {
+ dev_index = GetDeviceIndex(mod_, tensor_ty->vdevice.value());
+ scope = tensor_ty->vdevice.value()->memory_scope;
+ } else {
+ dev_index = GetDeviceIndexByScope(mod_, scope);
+ }
+
+ return builder_->Emit(Call(Type::Missing(), alloc_tensor_op,
+
{tensor_ty->shape.value().as_or_throw<ShapeExpr>(),
+ DataTypeImm(tensor_ty->dtype.value()->dtype),
+ IntImm::Int64(dev_index), StringImm(scope)},
+ Attrs(), {tensor_ty}),
+ "alloc");
+ }
+
+ void FlattenAndAllocateOutputs(const Type& type, const Op& alloc_tensor_op,
+ ffi::Array<Expr>* outs) {
+ if (const auto* tensor_ty = type.as<TensorTypeNode>()) {
+ outs->push_back(AllocateOutputTensor(ffi::GetRef<TensorType>(tensor_ty),
alloc_tensor_op));
+ return;
+ }
+ if (const auto* tuple_ty = type.as<TupleTypeNode>()) {
+ for (const Type& field : tuple_ty->fields) {
+ FlattenAndAllocateOutputs(field, alloc_tensor_op, outs);
+ }
+ return;
+ }
+ TVM_FFI_THROW(TypeError) << "call_tir expects nested tuple outputs to
contain only "
+ "TensorType, but got "
+ << type;
+ }
+
+ Expr RebuildOutputTuple(const Type& type, const ffi::Array<Expr>& outs,
size_t* index) {
+ if (type->IsInstance<TensorTypeNode>()) {
+ TVM_FFI_ICHECK_LT(*index, outs.size());
+ return outs[(*index)++];
+ }
+ if (const auto* tuple_ty = type.as<TupleTypeNode>()) {
+ ffi::Array<Expr> fields;
+ fields.reserve(tuple_ty->fields.size());
+ for (const Type& field : tuple_ty->fields) {
+ fields.push_back(RebuildOutputTuple(field, outs, index));
+ }
+ return Tuple(std::move(fields));
+ }
+ TVM_FFI_THROW(TypeError) << "call_tir expects nested tuple outputs to
contain only "
+ "TensorType, but got "
+ << type;
+ }
+
/*! \brief The context IRModule. */
IRModule mod_;
};
diff --git a/src/relax/transform/fuse_ops.cc b/src/relax/transform/fuse_ops.cc
index b76e129043..7d8986218b 100644
--- a/src/relax/transform/fuse_ops.cc
+++ b/src/relax/transform/fuse_ops.cc
@@ -396,7 +396,8 @@ class GraphCreator : public ExprVisitor {
*/
class FunctionCreator : public ExprMutator {
public:
- explicit FunctionCreator(bool lift_constant) : lift_constant_(lift_constant)
{}
+ explicit FunctionCreator(bool lift_constant, ffi::Map<Var, Expr>
outer_bindings)
+ : outer_bindings_(std::move(outer_bindings)),
lift_constant_(lift_constant) {}
/*!
* \brief Append a new binding to this function and possibly create new
parameters for the
* function accordingly
@@ -475,16 +476,15 @@ class FunctionCreator : public ExprMutator {
}
/*! \brief Set a var defined in the group as output. */
- size_t AppendOutput(const Var& var) {
+ void AppendOutput(const Var& var) {
TVM_FFI_ICHECK(defined_vars_.count(var.get()));
- auto output_idx = GetOutputIndex(var);
- if (output_idx) {
- return *output_idx;
- }
+ if (GetOutputIndex(var)) return;
output_vars_.push_back(var.get());
- return output_vars_.size() - 1;
}
+ /*! \brief Variables returned from the grouped function, in return-value
order. */
+ const std::vector<const VarNode*>& output_vars() const { return
output_vars_; }
+
/*!
* \brief Create the grouped function according to the collected bindings
and parameters
* \param composite_name The name to identify the pattern this function is
created from, if any.
@@ -622,6 +622,21 @@ class FunctionCreator : public ExprMutator {
// If the expression is not a variable or is a undefined variable, it
should be populated as a
// parameter of the relax function.
const auto* var = expr.as<VarNode>();
+ if (var != nullptr && defined_vars_.count(var) == 0) {
+ Var bound_var = ffi::GetRef<Var>(var);
+ Expr bound_value = bound_var;
+ std::unordered_set<const VarNode*> visited;
+ while (const auto* current_var = bound_value.as<VarNode>()) {
+ if (!visited.insert(current_var).second) break;
+ auto it = outer_bindings_.find(ffi::GetRef<Var>(current_var));
+ if (it == outer_bindings_.end()) break;
+ bound_value = (*it).second;
+ }
+ if (!bound_value.same_as(bound_var) &&
IsInlinableConstants(bound_value)) {
+ inlined_bindings_[var] = bound_value;
+ return;
+ }
+ }
if ((var == nullptr || defined_vars_.count(var) == 0) &&
(lift_constant_ || !expr->IsInstance<ConstantNode>())) {
ffi::String name = var != nullptr
@@ -650,6 +665,11 @@ class FunctionCreator : public ExprMutator {
if (it != arguments_.end()) {
return params_[it - arguments_.begin()];
}
+ if (const auto* var = expr.as<VarNode>()) {
+ if (auto inlined = inlined_bindings_.find(var); inlined !=
inlined_bindings_.end()) {
+ return inlined->second;
+ }
+ }
// Otherwise, recurse into this expression.
return ExprMutator::VisitExpr(expr);
}
@@ -674,10 +694,14 @@ class FunctionCreator : public ExprMutator {
private:
/*! \brief The variables defined in this function */
std::unordered_set<const VarNode*> defined_vars_;
+ /*! \brief Caller variables replaced by statically inlinable bound values. */
+ std::unordered_map<const VarNode*, Expr> inlined_bindings_;
/*! \brief The number of parameters reserved for constants */
int n_param_for_const_ = 0;
/*! \brief The output vars */
std::vector<const VarNode*> output_vars_;
+ /*! \brief Bindings in the caller function, used to inline static leaf
expressions. */
+ ffi::Map<Var, Expr> outer_bindings_;
/*! \brief Whether or not to lift bound constants to parameters */
bool lift_constant_;
/*! \brief Mapping from tuple parameter of the function to its position
index */
@@ -750,8 +774,10 @@ class OperatorFusor : public ExprMutator {
// attr::kCodegen.
if (func->IsInstance<relax::FunctionNode>() &&
!func->HasNonzeroAttr(attr::kPrimitive) &&
!func->GetAttr<ffi::String>(attr::kCodegen).has_value()) {
+ outer_bindings_ = AnalyzeVar2Value(func);
auto updated_func = VisitExpr(func).as_or_throw<Function>();
builder_->UpdateFunction(gv, updated_func);
+ outer_bindings_ = {};
}
}
return builder_->GetContextIRModule();
@@ -770,22 +796,6 @@ class OperatorFusor : public ExprMutator {
return obj2group;
}
- bool IsTupleOutput(Function f) {
- auto ty = GetType(f).as<FuncTypeNode>();
- TVM_FFI_ICHECK(ty);
- return ty->ret->IsInstance<TupleTypeNode>();
- }
-
- bool IsNestedTupleOutput(Function f) {
- if (!IsTupleOutput(f)) return false;
-
- auto tup = GetType(f).as<FuncTypeNode>()->ret.as<TupleTypeNode>();
- for (const auto& field : tup->fields) {
- if (field->IsInstance<TupleTypeNode>()) return true;
- }
- return false;
- }
-
BindingBlock VisitBindingBlock_(const DataflowBlockNode* block) final {
group2func_.clear();
@@ -808,9 +818,9 @@ class OperatorFusor : public ExprMutator {
// last binding of the group.
builder_->BeginDataflowBlock();
- // For each group, record which variables need to be remapped to the
output of TupleGetItem.
- // Only relevant when the output of the grouped function is a tuple.
- std::unordered_map<Group*, std::vector<Var>> pending_tuple_get;
+ // Preserve the original binding order when emitting TupleGetItem bindings
for groups with
+ // multiple boundary outputs. Missing entries are filled when the grouped
call is emitted.
+ std::unordered_map<Group*, std::vector<Var>> pending_output_remap;
// A grouped function which returns a tuple requires attaching
TupleGetItem to each element and
// remapping variables in earlier bindings appropriately. Thus, a binding
whose value depends on
@@ -837,15 +847,10 @@ class OperatorFusor : public ExprMutator {
}
const Function& func = func_info.function_.value();
- // If this binding belongs to a group whose output is a tuple, the
original bound variable
- // needs to be remapped to the output of TupleGetItem after the
corresponding tuple is
- // emitted.
- if (IsTupleOutput(func) && tuple_get_indices_.count(binding->var.get()))
{
- if (!GetType(binding->var)->IsInstance<TupleTypeNode>() ||
IsNestedTupleOutput(func)) {
- // When binding->var itself is a tuple, we do not need to remap this
variable to the
- // output of TupleGetItem unless the output is a nested tuple.
- pending_tuple_get[group].push_back(binding->var);
- }
+ const auto& output_vars = func_info.output_vars();
+ if (output_vars.size() > 1 && std::find(output_vars.begin(),
output_vars.end(),
+ binding->var.get()) !=
output_vars.end()) {
+ pending_output_remap[group].push_back(binding->var);
}
// Case 2. If the binding is not the last binding of the group, we skip
it.
@@ -863,29 +868,50 @@ class OperatorFusor : public ExprMutator {
GlobalVar gv = builder_->AddFunction(func, func_info.name_hint_);
// Step b. Create the call to the deduplicated function, and then emit
the call.
- // - If this binding is an output binding, emit an output variable.
- // - Otherwise, emit a dataflow variable.
+ // A multi-output call is internal to this dataflow block, while a
single-output call has the
+ // same dataflow/output status as its sole boundary variable. The last
binding is only the
+ // insertion point and may itself be a dead internal binding.
+ TVM_FFI_ICHECK(!output_vars.empty());
Var new_var;
Call call_to_emit = Call(Type::Missing(), gv,
UpdateArgs(func_info.arguments_));
- if (var_binding->var->IsInstance<DataflowVarNode>()) {
- new_var = builder_->Emit(call_to_emit);
- } else {
+ if (output_vars.size() == 1 &&
!output_vars[0]->IsInstance<DataflowVarNode>()) {
new_var = builder_->EmitOutput(call_to_emit);
+ } else {
+ new_var = builder_->Emit(call_to_emit);
}
- // Step c. Update the mapping used for the remapping of the binding
variables.
- if (IsTupleOutput(func) && !pending_tuple_get.empty()) {
- // If the output is a tuple, attach TupleGetItem to all tuple
elements, and
- // remap variables approriately.
- // The variables that need to be remapped and the corresponding tuple
indices are
- // available in pending_tuple_get and tuple_get_indices_ respectively.
- for (const auto& var : pending_tuple_get[group]) {
- auto tuple_get = TupleGetItem(new_var,
tuple_get_indices_[var.get()]);
- var_remap_[var] = builder_->Emit(tuple_get);
+ // Step c. Remap every boundary output to the corresponding result of
the grouped call.
+ // FunctionCreator uses output_vars() order when it constructs a
multi-output tuple. A
+ // single boundary output is returned directly, including when that
output is itself a tuple.
+ if (output_vars.size() == 1) {
+ var_remap_[ffi::GetRef<Var>(output_vars[0])] = new_var;
+ continue;
+ }
+
+ std::unordered_set<const VarNode*> remapped_outputs;
+ auto remap_output = [&](const Var& output_var) {
+ auto it = std::find(output_vars.begin(), output_vars.end(),
output_var.get());
+ TVM_FFI_ICHECK(it != output_vars.end());
+ int index = static_cast<int>(std::distance(output_vars.begin(), it));
+ TupleGetItem tuple_get(new_var, index);
+ if (output_var->IsInstance<DataflowVarNode>()) {
+ var_remap_[output_var] = builder_->Emit(tuple_get);
+ } else {
+ var_remap_[output_var] = builder_->EmitOutput(tuple_get);
+ }
+ remapped_outputs.insert(output_var.get());
+ };
+
+ if (auto it = pending_output_remap.find(group); it !=
pending_output_remap.end()) {
+ for (const Var& output_var : it->second) {
+ remap_output(output_var);
+ }
+ }
+ for (const VarNode* output_var : output_vars) {
+ if (!remapped_outputs.count(output_var)) {
+ remap_output(ffi::GetRef<Var>(output_var));
}
- } else {
- var_remap_[var_binding->var] = new_var;
}
}
// Step 5. Finish the binding block generation.
@@ -907,10 +933,8 @@ class OperatorFusor : public ExprMutator {
}
// Add the binding to the grouped function it's in, and update the
function information
// accordingly.
- if (!group2func_.count(group)) {
- group2func_.emplace(group, lift_constants_);
- }
- group2func_.find(group)->second.AppendBinding(binding);
+ auto it = group2func_.try_emplace(group, lift_constants_,
outer_bindings_).first;
+ it->second.AppendBinding(binding);
}
}
@@ -940,8 +964,7 @@ class OperatorFusor : public ExprMutator {
if (auto producer = group2func_.find(producer_group);
producer_group != cur_group && producer != group2func_.end()) {
- auto output_index = producer->second.AppendOutput(used_var);
- tuple_get_indices_[used_var.get()] = output_index;
+ producer->second.AppendOutput(used_var);
}
}
};
@@ -1040,9 +1063,8 @@ class OperatorFusor : public ExprMutator {
GroupMap obj2group_;
/*! \brief Internal function information map. */
std::unordered_map<Group*, FunctionCreator> group2func_;
- /*! \brief Record the index for TupleGetItem if the variable needs to be
remapped to an output
- * tuple element after fusion. */
- std::unordered_map<const VarNode*, int> tuple_get_indices_;
+ /*! \brief Bindings visible while rewriting the current Relax function. */
+ ffi::Map<Var, Expr> outer_bindings_;
/*!
* \brief A map from a group to its dependent groups, used to detect cyclic
dependencies.
* \note Use vector so we can be deterministic, there won't be a lot of dep
groups so
diff --git a/src/relax/transform/merge_composite_functions.cc
b/src/relax/transform/merge_composite_functions.cc
index 18e2736ecb..90a8bc2b95 100644
--- a/src/relax/transform/merge_composite_functions.cc
+++ b/src/relax/transform/merge_composite_functions.cc
@@ -56,6 +56,7 @@
#include <tvm/ffi/cast.h>
#include <tvm/ffi/reflection/registry.h>
+#include <tvm/relax/analysis.h>
#include <tvm/relax/expr_functor.h>
#include <tvm/relax/transform.h>
#include <tvm/relax/type.h>
@@ -81,17 +82,19 @@ class CompositeGroupsBuilder : public
MemoizedExprTranslator<Group*> {
CompositeGroupsBuilder(IRModule mod, support::Arena* arena) : mod_(mod),
arena_(arena) {}
GroupMap Run(Function func) {
+ var_usage_ = CollectVarUsage(func);
+ for (const auto& [var, value] : var_usage_.bound_values) {
+ value_to_bound_vars_[value.get()].push_back(var);
+ }
+
for (const auto& param : func->params) {
memo_[param] = arena_->make<Group>();
}
- PostOrderVisit(func, [this](Expr e) {
- // Make default groups for dataflow nodes other than CallNode.
- // Groups for CallNode are created in its visitor.
- if (e->IsInstance<ConstantNode>() || e->IsInstance<ShapeExprNode>() ||
- e->IsInstance<TupleNode>() || e->IsInstance<TupleGetItemNode>() ||
- (!e->IsInstance<CallNode>() && !e->IsInstance<VarNode>() &&
e.as<PrimExpr>())) {
- memo_[e] = arena_->make<Group>();
+ PostOrderVisit(func, [this](const Expr& expr) {
+ if (expr->IsInstance<ConstantNode>() ||
expr->IsInstance<ShapeExprNode>() ||
+ (!expr->IsInstance<CallNode>() && !expr->IsInstance<VarNode>() &&
expr.as<PrimExpr>())) {
+ memo_[expr] = arena_->make<Group>();
}
});
@@ -143,6 +146,9 @@ class CompositeGroupsBuilder : public
MemoizedExprTranslator<Group*> {
}
Group* VisitExpr_(const CallNode* call) {
+ for (const Expr& arg : call->args) {
+ EnsureVisited(arg);
+ }
std::vector<Group*> groups_to_merge = GetGroupsToMerge(call);
Group* group;
@@ -166,7 +172,65 @@ class CompositeGroupsBuilder : public
MemoizedExprTranslator<Group*> {
return group;
}
+ Group* VisitExpr_(const TupleNode* tuple) {
+ Expr tuple_expr = ffi::GetRef<Tuple>(tuple);
+ if (!IsFlatTensorTuple(tuple_expr)) return arena_->make<Group>();
+
+ for (const Expr& field : tuple->fields) {
+ EnsureVisited(field);
+ }
+ if (HasOnlyTupleGetItemUsers(tuple_expr)) {
+ Group* tuple_group = nullptr;
+ for (const Expr& field : tuple->fields) {
+ auto it = memo_.find(field);
+ if (it == memo_.end()) {
+ tuple_group = nullptr;
+ break;
+ }
+ Group* field_group = it->second->FindRoot();
+ if (tuple_group == nullptr) {
+ tuple_group = field_group;
+ } else if (tuple_group != field_group) {
+ tuple_group = nullptr;
+ break;
+ }
+ }
+ if (tuple_group != nullptr && CanAbsorbTupleNodes(tuple_group)) {
+ tuple_group->num_nodes += 1;
+ return tuple_group;
+ }
+ }
+
+ Group* group = arena_->make<Group>();
+ UpdateGroupDependencies(group, tuple->fields);
+ return group;
+ }
+
+ Group* VisitExpr_(const TupleGetItemNode* tuple_get_item) {
+ if (!IsFlatTensorTuple(tuple_get_item->tuple)) return
arena_->make<Group>();
+
+ EnsureVisited(tuple_get_item->tuple);
+ auto it = memo_.find(tuple_get_item->tuple);
+ if (it != memo_.end() && HasOnlyTupleGetItemUsers(tuple_get_item->tuple)) {
+ Group* tuple_group = it->second->FindRoot();
+ if (CanAbsorbTupleNodes(tuple_group)) {
+ tuple_group->num_nodes += 1;
+ return tuple_group;
+ }
+ }
+
+ Group* group = arena_->make<Group>();
+ UpdateGroupDependencies(group, {tuple_get_item->tuple});
+ return group;
+ }
+
private:
+ void EnsureVisited(const Expr& expr) {
+ if (!expr.as<GlobalVarNode>() && !memo_.count(expr)) {
+ VisitExpr(expr);
+ }
+ }
+
ffi::Optional<ffi::String> GetCodegenName(const Expr& callee) {
auto const* gvar = callee.as<GlobalVarNode>();
if (!gvar) {
@@ -189,6 +253,79 @@ class CompositeGroupsBuilder : public
MemoizedExprTranslator<Group*> {
return std::nullopt;
}
+ bool CanAbsorbTupleNodes(Group* group) { return
GetCodegenName(group->FindRoot()).has_value(); }
+
+ bool IsFlatTensorTuple(const Expr& expr) {
+ const auto* tuple_type = GetType(expr).as<TupleTypeNode>();
+ if (tuple_type == nullptr || tuple_type->fields.empty()) return false;
+ return std::all_of(tuple_type->fields.begin(), tuple_type->fields.end(),
+ [](const Type& field) { return
field->IsInstance<TensorTypeNode>(); });
+ }
+
+ bool HasOnlyTupleGetItemUsers(const Expr& tuple_expr) {
+ if (auto it = tuple_get_item_only_usage_.find(tuple_expr.get());
+ it != tuple_get_item_only_usage_.end()) {
+ return it->second;
+ }
+
+ bool result = ComputeHasOnlyTupleGetItemUsers(tuple_expr);
+ tuple_get_item_only_usage_[tuple_expr.get()] = result;
+ return result;
+ }
+
+ bool ComputeHasOnlyTupleGetItemUsers(const Expr& tuple_expr) {
+ ffi::Optional<Var> tuple_var;
+ if (const auto* var = tuple_expr.as<VarNode>()) {
+ tuple_var = ffi::GetRef<Var>(var);
+ } else if (auto it = value_to_bound_vars_.find(tuple_expr.get());
+ it != value_to_bound_vars_.end() && it->second.size() == 1) {
+ tuple_var = it->second[0];
+ }
+ if (!tuple_var.has_value()) return false;
+
+ // Follow aliases in both directions. Checking only tuple_var would allow
an alias to be used
+ // exclusively by TupleGetItem while the original tuple still escapes from
the external region.
+ std::vector<Var> pending{tuple_var.value()};
+ std::unordered_set<Var, ffi::ObjectPtrHash, ffi::ObjectPtrEqual> aliases;
+ bool has_tuple_get_item = false;
+ while (!pending.empty()) {
+ Var current = pending.back();
+ pending.pop_back();
+ if (!aliases.insert(current).second) continue;
+
+ if (std::any_of(var_usage_.outputs.begin(), var_usage_.outputs.end(),
+ [&](const Var& output) { return output.same_as(current);
})) {
+ return false;
+ }
+
+ // Walk toward the original tuple value when current is itself an alias.
+ if (auto binding_it = var_usage_.bound_values.find(current);
+ binding_it != var_usage_.bound_values.end()) {
+ if (const auto* source = (*binding_it).second.as<VarNode>()) {
+ pending.push_back(ffi::GetRef<Var>(source));
+ }
+ }
+
+ auto uses_it = var_usage_.downstream_usage.find(current);
+ if (uses_it == var_usage_.downstream_usage.end()) continue;
+ for (const Var& user : (*uses_it).second) {
+ auto binding_it = var_usage_.bound_values.find(user);
+ if (binding_it == var_usage_.bound_values.end()) return false;
+ const Expr& bound_value = (*binding_it).second;
+ if (const auto* tuple_get_item = bound_value.as<TupleGetItemNode>()) {
+ if (!tuple_get_item->tuple.same_as(current)) return false;
+ has_tuple_get_item = true;
+ } else if (const auto* source = bound_value.as<VarNode>()) {
+ if (!ffi::GetRef<Var>(source).same_as(current)) return false;
+ pending.push_back(user);
+ } else {
+ return false;
+ }
+ }
+ }
+ return has_tuple_get_item;
+ }
+
Group* CreateNewGroup(const CallNode* call) {
Group* group = arena_->make<Group>();
if (ffi::Optional<ffi::String> codegen_name = GetCodegenName(call->op)) {
@@ -226,6 +363,7 @@ class CompositeGroupsBuilder : public
MemoizedExprTranslator<Group*> {
std::unordered_set<Group*> dependencies;
for (const auto& arg : args) {
+ if (arg.as<GlobalVarNode>()) continue;
for (auto dep : group_deps_[memo_[arg]->FindRoot()]) {
dependencies.insert(dep);
}
@@ -279,6 +417,7 @@ class CompositeGroupsBuilder : public
MemoizedExprTranslator<Group*> {
std::unordered_set<Group*> parent_dependencies =
GetParentGroupDependencies(call->args);
for (const auto& arg : call->args) {
+ if (arg.as<GlobalVarNode>()) continue;
auto arg_group = memo_[arg];
ffi::Optional<ffi::String> arg_codegen_name = GetCodegenName(arg_group);
if (arg_codegen_name == codegen_name &&
!parent_dependencies.count(arg_group->FindRoot())) {
@@ -294,6 +433,9 @@ class CompositeGroupsBuilder : public
MemoizedExprTranslator<Group*> {
IRModule mod_;
support::Arena* arena_;
+ VarUsageInfo var_usage_;
+ std::unordered_map<const ffi::Object*, std::vector<Var>>
value_to_bound_vars_;
+ std::unordered_map<const ffi::Object*, bool> tuple_get_item_only_usage_;
// Map from group to its dependencies. All groups in this map, whether it's
// the key or in value, should be root node (that is, group->parent ==
nullptr).
std::unordered_map<Group*, std::unordered_set<Group*>> group_deps_;
diff --git a/src/runtime/extra/contrib/tensorrt/tensorrt_ops.cc
b/src/runtime/extra/contrib/tensorrt/tensorrt_ops.cc
index 61dee5a0fb..0004b19124 100644
--- a/src/runtime/extra/contrib/tensorrt/tensorrt_ops.cc
+++ b/src/runtime/extra/contrib/tensorrt/tensorrt_ops.cc
@@ -195,6 +195,23 @@ class ActivationOpConverter : public TensorRTOpConverter {
}
};
+class SiluOpConverter : public TensorRTOpConverter {
+ public:
+ explicit SiluOpConverter(std::string op_name)
+ : TensorRTOpConverter(std::move(op_name), {kTensor}) {}
+ ~SiluOpConverter() = default;
+
+ void Convert(TensorRTOpConverterParams* params) const {
+ auto input = params->inputs.at(0).tensor;
+ auto sigmoid_layer = params->network->addActivation(*input,
nvinfer1::ActivationType::kSIGMOID);
+ TVM_FFI_ICHECK(sigmoid_layer != nullptr);
+ auto silu_layer = params->network->addElementWise(*input,
*sigmoid_layer->getOutput(0),
+
nvinfer1::ElementWiseOperation::kPROD);
+ TVM_FFI_ICHECK(silu_layer != nullptr);
+ params->outputs.push_back(silu_layer->getOutput(0));
+ }
+};
+
class ElementWiseBinaryOpConverter : public TensorRTOpConverter {
public:
explicit ElementWiseBinaryOpConverter(std::string op_name)
@@ -1237,6 +1254,84 @@ class StridedSliceOpConverter : public
TensorRTOpConverter {
};
#endif
+#if TRT_VERSION_GE(8, 0, 0)
+class Resize2DOpConverter : public TensorRTOpConverter {
+ public:
+ explicit Resize2DOpConverter(std::string op_name)
+ : TensorRTOpConverter(std::move(op_name), {kTensor}) {}
+ ~Resize2DOpConverter() = default;
+
+ void Convert(TensorRTOpConverterParams* params) const {
+ auto input = params->inputs.at(0).tensor;
+ TVM_FFI_ICHECK_EQ(params->node.GetAttr<ffi::String>("layout"), "NCHW");
+ auto input_dims = TrtDimsToVector(input->getDimensions());
+ TVM_FFI_ICHECK_GE(input_dims.size(), 2);
+ // Relax resize2d takes the target (height, width) as a Shape argument
(serialized as arg_size).
+ auto size = params->node.GetAttr<ffi::Array<int64_t>>("arg_size");
+ TVM_FFI_ICHECK_EQ(size.size(), 2);
+ const std::string method = params->node.GetAttr<ffi::String>("method");
+ const std::string coordinate_transformation_mode =
+ params->node.GetAttr<ffi::String>("coordinate_transformation_mode");
+ const std::string rounding_method =
params->node.GetAttr<ffi::String>("rounding_method");
+
+ auto resize_layer = params->network->addResize(*input);
+ TVM_FFI_ICHECK(resize_layer != nullptr);
+
+ static const std::unordered_map<std::string, nvinfer1::InterpolationMode>
method_map = {
+ {"nearest_neighbor", nvinfer1::InterpolationMode::kNEAREST},
+ {"linear", nvinfer1::InterpolationMode::kLINEAR},
+ {"cubic", nvinfer1::InterpolationMode::kCUBIC}};
+ auto method_it = method_map.find(method);
+ TVM_FFI_ICHECK(method_it != method_map.end()) << "Unsupported resize2d
method " << method;
+ resize_layer->setResizeMode(method_it->second);
+
+ // pytorch_half_pixel matches half_pixel for output extents greater than
one. Singleton output
+ // dimensions are handled by the selector below.
+ static const std::unordered_map<std::string,
nvinfer1::ResizeCoordinateTransformation>
+ coordinate_transformation_map = {
+ {"asymmetric",
nvinfer1::ResizeCoordinateTransformation::kASYMMETRIC},
+ {"align_corners",
nvinfer1::ResizeCoordinateTransformation::kALIGN_CORNERS},
+ {"half_pixel",
nvinfer1::ResizeCoordinateTransformation::kHALF_PIXEL},
+ {"pytorch_half_pixel",
nvinfer1::ResizeCoordinateTransformation::kHALF_PIXEL}};
+ auto coordinate_transformation_it =
+ coordinate_transformation_map.find(coordinate_transformation_mode);
+ TVM_FFI_ICHECK(coordinate_transformation_it !=
coordinate_transformation_map.end())
+ << "Unsupported resize2d coordinate_transformation_mode " <<
coordinate_transformation_mode;
+
resize_layer->setCoordinateTransformation(coordinate_transformation_it->second);
+ if (coordinate_transformation_mode == "pytorch_half_pixel") {
+ // PyTorch maps an output dimension of size one to source coordinate
zero, whereas the
+ // regular half-pixel formula selects the center of the input dimension.
+
resize_layer->setSelectorForSinglePixel(nvinfer1::ResizeSelector::kUPPER);
+ }
+
+ if (method == "nearest_neighbor") {
+ static const std::unordered_map<std::string, nvinfer1::ResizeRoundMode>
rounding_map = {
+ {"floor", nvinfer1::ResizeRoundMode::kFLOOR},
+ {"ceil", nvinfer1::ResizeRoundMode::kCEIL},
+ {"round_prefer_ceil", nvinfer1::ResizeRoundMode::kHALF_UP},
+ {"round_prefer_floor", nvinfer1::ResizeRoundMode::kHALF_DOWN}};
+ auto rounding_it = rounding_map.find(rounding_method);
+ TVM_FFI_ICHECK(rounding_it != rounding_map.end())
+ << "Unsupported resize2d rounding_method " << rounding_method;
+ resize_layer->setNearestRounding(rounding_it->second);
+ }
+
+ if (method == "cubic") {
+
resize_layer->setCubicCoeff(static_cast<float>(params->node.GetAttr<double>("cubic_alpha")));
+ resize_layer->setExcludeOutside(
+ static_cast<bool>(params->node.GetAttr<int64_t>("cubic_exclude")));
+ }
+
+ std::vector<int> output_dims(input_dims.begin(), input_dims.end());
+ output_dims[output_dims.size() - 2] = static_cast<int>(size[0]);
+ output_dims[output_dims.size() - 1] = static_cast<int>(size[1]);
+ resize_layer->setOutputDimensions(VectorToTrtDims(output_dims));
+
+ params->outputs.push_back(resize_layer->getOutput(0));
+ }
+};
+#endif // TRT_VERSION_GE(8, 0, 0)
+
class AdaptivePoolingOpConverter : public TensorRTOpConverter {
public:
explicit AdaptivePoolingOpConverter(std::string op_name)
@@ -1291,6 +1386,7 @@ const std::unordered_map<std::string,
std::unique_ptr<TensorRTOpConverter>>& Get
all_converters.emplace_back(std::make_unique<ActivationOpConverter>("nn.relu"));
all_converters.emplace_back(std::make_unique<ActivationOpConverter>("sigmoid"));
all_converters.emplace_back(std::make_unique<ActivationOpConverter>("tanh"));
+ all_converters.emplace_back(std::make_unique<SiluOpConverter>("nn.silu"));
all_converters.emplace_back(std::make_unique<BatchNormOpConverter>("nn.batch_norm"));
all_converters.emplace_back(std::make_unique<LayerNormOpConverter>("nn.layer_norm"));
all_converters.emplace_back(std::make_unique<SoftmaxOpConverter>("nn.softmax"));
@@ -1356,6 +1452,9 @@ const std::unordered_map<std::string,
std::unique_ptr<TensorRTOpConverter>>& Get
#if TRT_VERSION_GE(7, 0, 0)
all_converters.emplace_back(std::make_unique<UnaryOpConverter>("erf"));
#endif // TRT_VERSION_GE(7, 0, 0)
+#if TRT_VERSION_GE(8, 0, 0)
+
all_converters.emplace_back(std::make_unique<Resize2DOpConverter>("image.resize2d"));
+#endif // TRT_VERSION_GE(8, 0, 0)
auto* map = new std::unordered_map<std::string,
std::unique_ptr<TensorRTOpConverter>>();
for (auto& converter : all_converters) {
map->emplace("tensorrt." + converter->op_name, std::move(converter));
diff --git a/tests/python/relax/test_backend_tensorrt.py
b/tests/python/relax/test_backend_tensorrt.py
new file mode 100644
index 0000000000..722481cc0c
--- /dev/null
+++ b/tests/python/relax/test_backend_tensorrt.py
@@ -0,0 +1,117 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import pytest
+
+from tvm import relax
+from tvm.relax.backend.contrib.tensorrt import partition_for_tensorrt
+
+
+def _make_resize2d_module(
+ input_shape=(1, 3, 8, 8),
+ input_dtype="float32",
+ size=(16, 16),
+ *,
+ dynamic_size=False,
+ bind_static_size=False,
+ layout="NCHW",
+ method="linear",
+ coordinate_transformation_mode="half_pixel",
+ rounding_method="round",
+ out_dtype=None,
+):
+ builder = relax.BlockBuilder()
+ data = relax.Var("data", relax.TensorType(input_shape, input_dtype))
+ params = [data]
+ if dynamic_size:
+ size_expr = relax.Var("size", relax.ShapeType(ndim=2))
+ params.append(size_expr)
+ else:
+ size_expr = relax.ShapeExpr(size)
+
+ with builder.function("main", params):
+ if bind_static_size:
+ size_expr = builder.emit(size_expr, "size")
+ with builder.dataflow():
+ output = builder.emit(
+ relax.op.image.resize2d(
+ data,
+ size=size_expr,
+ layout=layout,
+ method=method,
+
coordinate_transformation_mode=coordinate_transformation_mode,
+ rounding_method=rounding_method,
+ out_dtype=out_dtype,
+ )
+ )
+ output = builder.emit_output(output)
+ builder.emit_func_output(output)
+ return builder.get()
+
+
+def _tensorrt_regions(mod):
+ return [
+ func
+ for func in mod.functions.values()
+ if isinstance(func, relax.Function)
+ and func.attrs is not None
+ and func.attrs.get("Codegen") == "tensorrt"
+ ]
+
+
+def test_resize2d_partition_supported():
+ mod = _make_resize2d_module(
+ size=(13, 11),
+ bind_static_size=True,
+ method="nearest_neighbor",
+ coordinate_transformation_mode="asymmetric",
+ rounding_method="floor",
+ )
+ partitioned = partition_for_tensorrt(mod)
+
+ regions = _tensorrt_regions(partitioned)
+ assert len(regions) == 1
+ assert len(regions[0].params) == 1
+
+
[email protected](
+ "kwargs",
+ [
+ pytest.param({"input_shape": (1, 8, 8, 3), "layout": "NHWC"},
id="unsupported-layout"),
+ pytest.param({"dynamic_size": True}, id="dynamic-size"),
+ pytest.param({"input_dtype": "float64"}, id="unsupported-input-dtype"),
+ pytest.param({"out_dtype": "float16"}, id="different-output-dtype"),
+ pytest.param(
+ {
+ "method": "nearest_neighbor",
+ "coordinate_transformation_mode": "tf_half_pixel_for_nn",
+ "rounding_method": "floor",
+ },
+ id="unsupported-coordinate-mode",
+ ),
+ pytest.param(
+ {
+ "method": "nearest_neighbor",
+ "coordinate_transformation_mode": "asymmetric",
+ "rounding_method": "round",
+ },
+ id="ties-to-even-rounding",
+ ),
+ ],
+)
+def test_resize2d_partition_fallback(kwargs):
+ partitioned = partition_for_tensorrt(_make_resize2d_module(**kwargs))
+ assert not _tensorrt_regions(partitioned)
diff --git a/tests/python/relax/test_codegen_tensorrt.py
b/tests/python/relax/test_codegen_tensorrt.py
index 73e3471b20..4a3c35ebd7 100644
--- a/tests/python/relax/test_codegen_tensorrt.py
+++ b/tests/python/relax/test_codegen_tensorrt.py
@@ -21,6 +21,7 @@ import tvm
import tvm.testing
from tvm import relax
from tvm.contrib.pickle_memoize import memoize
+from tvm.relax.backend.contrib.tensorrt import partition_for_tensorrt
from tvm.relax.dpl import is_op, make_fused_bias_activation_pattern, wildcard
from tvm.script import relax as R
from tvm.testing import env
@@ -128,13 +129,13 @@ def _offload_and_compare(mod, params_np, patterns,
data_np, rtol=1e-2, atol=1e-2
otherwise collapse repeated ops.
"""
ref = build_and_run(mod, [data_np, *params_np.values()], "llvm",
legalize=True)
- partitioned = tvm.transform.Sequential(
- [
- relax.transform.BindParams("main", params_np),
- relax.transform.FuseOpsByPattern(patterns),
- relax.transform.MergeCompositeFunctions(),
- ]
- )(mod)
+ mod = relax.transform.BindParams("main", params_np)(mod)
+ if patterns is None:
+ partitioned = partition_for_tensorrt(mod)
+ else:
+ partitioned = tvm.transform.Sequential(
+ [relax.transform.FuseOpsByPattern(patterns),
relax.transform.MergeCompositeFunctions()]
+ )(mod)
# Guard against a silent false pass: if no pattern matched, nothing is
offloaded and the
# comparison would trivially succeed via the TVM fallback without
exercising the converter.
assert any(
@@ -224,6 +225,26 @@ def test_tensorrt_sigmoid():
_offload_and_compare(Sigmoid, {}, patterns, data)
+def test_tensorrt_subtract_constant_lhs():
+ """A bound constant on the left-hand side must remain the minuend."""
+
+ @tvm.script.ir_module
+ class ConstantMinusTensor:
+ @R.function
+ def main(
+ data: R.Tensor((2, 3, 4), "float32"),
+ constant: R.Tensor((2, 3, 4), "float32"),
+ ):
+ with R.dataflow():
+ out = relax.op.subtract(constant, data)
+ R.output(out)
+ return out
+
+ data = np.linspace(-2.0, 3.0, 24, dtype="float32").reshape(2, 3, 4)
+ constant = np.linspace(4.0, 9.0, 24, dtype="float32").reshape(2, 3, 4)
+ _offload_and_compare(ConstantMinusTensor, {"constant": constant}, None,
data)
+
+
def test_tensorrt_tanh():
@tvm.script.ir_module
class Tanh:
@@ -475,18 +496,13 @@ def test_tensorrt_split():
def main(data: R.Tensor((4, 8, 16), "float32")):
with R.dataflow():
parts = relax.op.split(data, 2, axis=1)
- out = relax.op.add(parts[0], parts[1])
+ out = relax.op.subtract(parts[1], parts[0])
R.output(out)
return out
- data = np.random.randn(4, 8, 16).astype("float32")
- # Offload the add too so both split outputs are consumed inside TensorRT
(and nothing is left
- # for the VM to legalize).
- patterns = [
- ("tensorrt.split", is_op("relax.split")(wildcard())),
- ("tensorrt.add", is_op("relax.add")(wildcard(), wildcard())),
- ]
- _offload_and_compare(Split, {}, patterns, data)
+ data = np.ones((4, 8, 16), dtype="float32")
+ data[:, 4:, :] = 5
+ _offload_and_compare(Split, {}, None, data)
def test_tensorrt_layout_transform():
@@ -640,5 +656,78 @@ def test_partition_for_tensorrt():
tvm.testing.assert_allclose(out, ref, rtol=1e-2, atol=1e-2)
[email protected](
+ "out_hw, method, coordinate_transformation_mode, rounding_method,
cubic_alpha",
+ [
+ ((13, 11), "nearest_neighbor", "asymmetric", "floor", -0.75),
+ ((13, 11), "linear", "half_pixel", "round", -0.75),
+ ((16, 16), "cubic", "half_pixel", "round", -0.5),
+ ],
+)
+def test_tensorrt_resize2d(
+ out_hw, method, coordinate_transformation_mode, rounding_method,
cubic_alpha
+):
+ """Regression test for image.resize2d offload in #19887."""
+
+ @tvm.script.ir_module
+ class Resize2D:
+ @R.function
+ def main(data: R.Tensor((1, 3, 8, 8), "float32")):
+ with R.dataflow():
+ out = relax.op.image.resize2d(
+ data,
+ size=out_hw,
+ layout="NCHW",
+ method=method,
+
coordinate_transformation_mode=coordinate_transformation_mode,
+ rounding_method=rounding_method,
+ cubic_alpha=cubic_alpha,
+ )
+ R.output(out)
+ return out
+
+ data = np.random.randn(1, 3, 8, 8).astype("float32")
+ _offload_and_compare(Resize2D, {}, None, data)
+
+
+def test_tensorrt_resize2d_pytorch_half_pixel_single_dimension():
+ """pytorch_half_pixel selects source coordinate zero for each singleton
output dimension."""
+
+ @tvm.script.ir_module
+ class ResizeSingleDimension:
+ @R.function
+ def main(data: R.Tensor((1, 1, 5, 7), "float32")):
+ with R.dataflow():
+ out = relax.op.image.resize2d(
+ data,
+ size=(1, 1),
+ method="nearest_neighbor",
+ coordinate_transformation_mode="pytorch_half_pixel",
+ rounding_method="floor",
+ )
+ R.output(out)
+ return out
+
+ mod = ResizeSingleDimension
+ data = np.arange(35, dtype="float32").reshape(1, 1, 5, 7)
+ _offload_and_compare(mod, {}, None, data, rtol=0, atol=0)
+
+
+def test_tensorrt_silu():
+ """YOLO's SiLU activation is lowered as x * sigmoid(x)."""
+
+ @tvm.script.ir_module
+ class Silu:
+ @R.function
+ def main(data: R.Tensor((1, 16, 8, 8), "float32")):
+ with R.dataflow():
+ out = relax.op.nn.silu(data)
+ R.output(out)
+ return out
+
+ data = np.random.randn(1, 16, 8, 8).astype("float32")
+ _offload_and_compare(Silu, {}, None, data)
+
+
if __name__ == "__main__":
tvm.testing.main()
diff --git a/tests/python/relax/test_op_create.py
b/tests/python/relax/test_op_create.py
index c9981de37a..0e8d403bae 100644
--- a/tests/python/relax/test_op_create.py
+++ b/tests/python/relax/test_op_create.py
@@ -567,6 +567,11 @@ def test_arange_infer_ty():
_check_inference(bb, relax.op.arange(1.0, 10), relax.TensorType((9,),
"float32"))
_check_inference(bb, relax.op.arange(0, 20, 2.5), relax.TensorType((8,),
"float32"))
_check_inference(bb, relax.op.arange(1, 10, 2.3), relax.TensorType((4,),
"float32"))
+ _check_inference(
+ bb,
+ relax.op.arange(0, 10, 1, tvm.ir.PrimType("float32")),
+ relax.TensorType((10,), "float32"),
+ )
def test_arange_infer_ty_shape_var():
diff --git a/tests/python/relax/test_transform.py
b/tests/python/relax/test_transform.py
index d32caa69d5..dd811855d8 100644
--- a/tests/python/relax/test_transform.py
+++ b/tests/python/relax/test_transform.py
@@ -356,6 +356,50 @@ def test_call_dps_packed_rewrite():
assert s2.op.global_symbol == "test.op.identity"
+def test_call_dps_packed_rewrite_nested_tuple_output():
+ """Flatten nested outputs for the packed ABI, then rebuild their Relax
structure."""
+ input_ty = relax.TensorType((2, 3), "float32")
+ flat_output_types = [
+ relax.TensorType((2, 3), "float32"),
+ relax.TensorType((4,), "int32"),
+ relax.TensorType((5, 6), "float16"),
+ ]
+ output_ty = tvm.ir.TupleType([flat_output_types[0],
tvm.ir.TupleType(flat_output_types[1:])])
+
+ x = relax.Var("x", input_ty)
+ call = relax.Call(
+ tvm.ir.Op.get("relax.call_dps_packed"),
+ [relax.ExternFunc("test.op.nested_outputs"), relax.Tuple([x])],
+ ty_args=[output_ty],
+ )
+ builder = relax.BlockBuilder()
+ with builder.function("main", [x], attrs={"relax.force_pure": True}):
+ out = builder.emit(call)
+ builder.emit_func_output(out)
+
+ after = relax.transform.CallTIRRewrite()(builder.get())
+ relax.analysis.well_formed(after)
+ func = after["main"]
+ block = func.body.blocks[0]
+
+ alloc_bindings = block.bindings[:3]
+ for binding, expected_ty in zip(alloc_bindings, flat_output_types):
+ assert binding.value.op.name == "relax.builtin.alloc_tensor"
+ tvm.ir.assert_structural_equal(binding.var.ty, expected_ty)
+
+ packed_call = block.bindings[3].value
+ assert packed_call.op.global_symbol == "test.op.nested_outputs"
+ assert packed_call.args[0].same_as(func.params[0])
+ assert all(
+ arg.same_as(binding.var) for arg, binding in zip(packed_call.args[1:],
alloc_bindings)
+ )
+
+ rebuilt = block.bindings[4].value
+ assert rebuilt.fields[0].same_as(alloc_bindings[0].var)
+ assert rebuilt.fields[1].fields[0].same_as(alloc_bindings[1].var)
+ assert rebuilt.fields[1].fields[1].same_as(alloc_bindings[2].var)
+
+
def test_call_tir_inplace_simple():
# simple case: one inplace argument
@tvm.script.ir_module
diff --git a/tests/python/relax/test_transform_fuse_ops_by_pattern.py
b/tests/python/relax/test_transform_fuse_ops_by_pattern.py
index 05102dd628..52e5f40d47 100644
--- a/tests/python/relax/test_transform_fuse_ops_by_pattern.py
+++ b/tests/python/relax/test_transform_fuse_ops_by_pattern.py
@@ -1422,5 +1422,81 @@ def test_concat():
check(mod, [("x.concat", pat_clip)], Expected2)
+def test_unique_boundary_output_precedes_last_group_binding():
+ """Export a sole boundary output even when a dead internal binding follows
it."""
+
+ @I.ir_module
+ class Before:
+ @R.function
+ def main(
+ x: R.Tensor((2, 4), "float32"),
+ ) -> R.Tensor((2, 4), "float32"):
+ with R.dataflow():
+ first = R.nn.relu(x)
+ kept = R.nn.relu(first)
+ dead = R.nn.relu(kept)
+ R.output(kept)
+ return kept
+
+ pattern =
is_op("relax.nn.relu")(is_op("relax.nn.relu")(is_op("relax.nn.relu")(wildcard())))
+ after = relax.transform.FuseOpsByPattern(
+ [("compiler_A.relu_chain", pattern)], annotate_codegen=True
+ )(Before)
+
+ relax.analysis.well_formed(after)
+ assert not relax.analysis.free_vars(after["main"])
+
+ calls = [
+ binding
+ for block in after["main"].body.blocks
+ for binding in block.bindings
+ if isinstance(binding.value, relax.Call)
+ and isinstance(binding.value.op, relax.GlobalVar)
+ and after[binding.value.op].attrs.get("Codegen") == "compiler_A"
+ ]
+ assert len(calls) == 1
+ grouped_result = calls[0].var
+ assert not isinstance(grouped_result, relax.DataflowVar)
+ assert after["main"].body.body.same_as(grouped_result)
+
+
+def test_inline_bound_static_shape_argument():
+ """A static leaf binding should not become a grouped-function parameter."""
+
+ @I.ir_module
+ class Before:
+ @R.function
+ def main(x: R.Tensor((4,), "float32")) -> R.Tensor((2, 2), "float32"):
+ shape: R.Shape([2, 2]) = R.shape([2, 2])
+ with R.dataflow():
+ out: R.Tensor((2, 2), "float32") = R.reshape(x, shape)
+ R.output(out)
+ return out
+
+ pattern = is_op("relax.reshape")(wildcard(), wildcard())
+ after = relax.transform.FuseOpsByPattern([("compiler_A.reshape",
pattern)])(Before)
+ grouped = [
+ func
+ for func in after.functions.values()
+ if isinstance(func, relax.Function)
+ and func.attrs is not None
+ and func.attrs.get("Composite") == "compiler_A.reshape"
+ ]
+ assert len(grouped) == 1
+ assert len(grouped[0].params) == 1
+
+ reshape_calls = []
+ relax.analysis.post_order_visit(
+ grouped[0],
+ lambda expr: reshape_calls.append(expr)
+ if isinstance(expr, relax.Call)
+ and isinstance(expr.op, tvm.ir.Op)
+ and expr.op.name == "relax.reshape"
+ else None,
+ )
+ assert len(reshape_calls) == 1
+ assert isinstance(reshape_calls[0].args[1], relax.ShapeExpr)
+
+
if __name__ == "__main__":
pytest.main([__file__])
diff --git a/tests/python/relax/test_transform_merge_composite_functions.py
b/tests/python/relax/test_transform_merge_composite_functions.py
index b1ce09799a..86019e706e 100644
--- a/tests/python/relax/test_transform_merge_composite_functions.py
+++ b/tests/python/relax/test_transform_merge_composite_functions.py
@@ -931,6 +931,16 @@ def check(mod, expected):
tvm.ir.assert_structural_equal(partitioned, expected)
+def get_codegen_regions(mod):
+ return {
+ gvar.name_hint: str(func.attrs["Codegen"])
+ for gvar, func in mod.functions.items()
+ if isinstance(func, relax.Function)
+ and func.attrs is not None
+ and func.attrs.get("Codegen") is not None
+ }
+
+
def test_conv2d_relu_x2():
check(Conv2dReLUx2, Conv2dReLUx2_merged)
@@ -1221,5 +1231,181 @@ def test_handle_existence_of_call_tir():
tvm.ir.assert_structural_equal(Expected, After)
+def test_tuple_projection_merging():
+ """Merge projections without changing their indices; visit inline tuple
arguments."""
+
+ @tvm.script.ir_module
+ class Before:
+ @R.function(private=True)
+ def split(
+ x: R.Tensor((2, 4), "float32"),
+ ) -> R.Tuple(R.Tensor((1, 4), "float32"), R.Tensor((1, 4), "float32")):
+ R.func_attr({"Composite": "compiler_A.split", "Primitive": True})
+ return R.split(x, indices_or_sections=2, axis=0)
+
+ @R.function(private=True)
+ def subtract(
+ x: R.Tensor((1, 4), "float32"),
+ y: R.Tensor((1, 4), "float32"),
+ ) -> R.Tensor((1, 4), "float32"):
+ R.func_attr({"Composite": "compiler_A.subtract", "Primitive":
True})
+ return R.subtract(x, y)
+
+ @R.function
+ def main(x: R.Tensor((2, 4), "float32")) -> R.Tensor((2, 4),
"float32"):
+ cls = Before
+ with R.dataflow():
+ parts = cls.split(x)
+ left = parts[0]
+ right = parts[1]
+ repacked = R.tuple(left, right)
+ reordered_left = repacked[0]
+ reordered_right = repacked[1]
+ difference = cls.subtract(reordered_right, reordered_left)
+ out = R.concat((difference, difference), axis=0)
+ R.output(out)
+ return out
+
+ after = relax.transform.MergeCompositeFunctions()(Before)
+ assert get_codegen_regions(after) == {"fused_split_subtract_compiler_A":
"compiler_A"}
+ relax.analysis.well_formed(after)
+
+ region = next(
+ func
+ for func in after.functions.values()
+ if isinstance(func, relax.Function)
+ and func.attrs is not None
+ and func.attrs.get("Codegen") == "compiler_A"
+ )
+ bindings = [binding for block in region.body.blocks for binding in
block.bindings]
+ bound_values = {binding.var: binding.value for binding in bindings}
+ subtract_var = next(
+ var
+ for var, value in bound_values.items()
+ if isinstance(value, relax.Function)
+ and value.attrs is not None
+ and value.attrs.get("Composite") == "compiler_A.subtract"
+ )
+ subtract_call = next(
+ value
+ for value in bound_values.values()
+ if isinstance(value, relax.Call) and value.op.same_as(subtract_var)
+ )
+ assert [bound_values[arg].index for arg in subtract_call.args] == [1, 0]
+
+
+def test_tuple_projection_preserves_real_cycle_boundary():
+ """Transparent projections must not bridge a dependency through another
codegen."""
+
+ @tvm.script.ir_module
+ class Before:
+ @R.function(private=True)
+ def split(
+ x: R.Tensor((2, 4), "float32"),
+ ) -> R.Tuple(R.Tensor((1, 4), "float32"), R.Tensor((1, 4), "float32")):
+ R.func_attr({"Composite": "compiler_A.split", "Primitive": True})
+ return R.split(x, indices_or_sections=2, axis=0)
+
+ @R.function(private=True)
+ def foreign_relu(
+ x: R.Tensor((1, 4), "float32"),
+ ) -> R.Tensor((1, 4), "float32"):
+ R.func_attr({"Composite": "compiler_B.relu", "Primitive": True})
+ return R.nn.relu(x)
+
+ @R.function(private=True)
+ def add(
+ x: R.Tensor((1, 4), "float32"),
+ y: R.Tensor((1, 4), "float32"),
+ ) -> R.Tensor((1, 4), "float32"):
+ R.func_attr({"Composite": "compiler_A.add", "Primitive": True})
+ return R.add(x, y)
+
+ @R.function
+ def main(x: R.Tensor((2, 4), "float32")) -> R.Tensor((1, 4),
"float32"):
+ cls = Before
+ with R.dataflow():
+ parts = cls.split(x)
+ left = parts[0]
+ right = parts[1]
+ foreign = cls.foreign_relu(left)
+ out = cls.add(right, foreign)
+ R.output(out)
+ return out
+
+ after = relax.transform.MergeCompositeFunctions()(Before)
+ assert get_codegen_regions(after) == {
+ "fused_add_compiler_A": "compiler_A",
+ "fused_foreign_relu_compiler_B": "compiler_B",
+ "fused_split_compiler_A": "compiler_A",
+ }
+ relax.analysis.well_formed(after)
+
+
+def test_tuple_projection_rejects_escaping_tuple():
+ """Keep the projection outside when the complete tuple also crosses the
region boundary."""
+
+ @tvm.script.ir_module
+ class Before:
+ @R.function(private=True)
+ def split(
+ x: R.Tensor((2, 4), "float32"),
+ ) -> R.Tuple(R.Tensor((1, 4), "float32"), R.Tensor((1, 4), "float32")):
+ R.func_attr({"Composite": "compiler_A.split", "Primitive": True})
+ return R.split(x, indices_or_sections=2, axis=0)
+
+ @R.function(private=True)
+ def relu(
+ x: R.Tensor((1, 4), "float32"),
+ ) -> R.Tensor((1, 4), "float32"):
+ R.func_attr({"Composite": "compiler_A.relu", "Primitive": True})
+ return R.nn.relu(x)
+
+ @R.function
+ def main(
+ x: R.Tensor((2, 4), "float32"),
+ ) -> R.Tuple(
+ R.Tuple(R.Tensor((1, 4), "float32"), R.Tensor((1, 4), "float32")),
+ R.Tensor((1, 4), "float32"),
+ ):
+ cls = Before
+ with R.dataflow():
+ parts = cls.split(x)
+ alias = parts
+ left = alias[0]
+ out = cls.relu(left)
+ result = R.tuple(alias, out)
+ R.output(result)
+ return result
+
+ after = relax.transform.MergeCompositeFunctions()(Before)
+ assert get_codegen_regions(after) == {
+ "fused_relu_compiler_A": "compiler_A",
+ "fused_split_compiler_A": "compiler_A",
+ }
+ relax.analysis.well_formed(after)
+
+ bindings = [binding for block in after["main"].body.blocks for binding in
block.bindings]
+ split_result = next(
+ binding.var
+ for binding in bindings
+ if isinstance(binding.value, relax.Call)
+ and isinstance(binding.value.op, relax.GlobalVar)
+ and binding.value.op.name_hint == "fused_split_compiler_A"
+ )
+ projections = [
+ binding.value
+ for binding in bindings
+ if isinstance(binding.value, relax.TupleGetItem)
+ and binding.value.tuple_value.same_as(split_result)
+ ]
+ assert [projection.index for projection in projections] == [0]
+
+ result_var = after["main"].body.body
+ result_value = next(binding.value for binding in bindings if
binding.var.same_as(result_var))
+ assert isinstance(result_value, relax.Tuple)
+ assert result_value.fields[0].same_as(split_result)
+
+
if __name__ == "__main__":
pytest.main([__file__])