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 1fb1c38665 [IR][Relax] Include expression types in structural identity
(#19933)
1fb1c38665 is described below
commit 1fb1c3866501eee8ddb5f9e01b53603c2fc805dc
Author: Tianqi Chen <[email protected]>
AuthorDate: Sat Jul 4 22:39:52 2026 +0800
[IR][Relax] Include expression types in structural identity (#19933)
## Rationale
After `PrimExpr` and `Expr` share one typed expression hierarchy,
expression types are part of semantic identity. Structurally identical
syntax with different types compare and hash differently, while source
spans remain diagnostic metadata.
## Invariant
`ExprNode::ty` participates in structural equality and hashing by
default. `GlobalVar` and Relax variables retain their symbol identity
rules. `tirx.PrimFunc` compares and hashes authoritative source fields
while excluding its derived type cache until all transformation paths
maintain that cache eagerly. Nested symbolic-shape rendering is isolated
from outer diagnostic configuration so diagnostic context cannot become
script-token content.
## Changes
- include expression types in generic structural equality and hashing
- preserve GlobalVar and Relax variable identity plus definition-safe
SeqExpr traversal
- compare and hash PrimFunc from authoritative fields while excluding
its stale derived type cache
- normalize narrow Relax construction and expected-fixture types exposed
by stricter identity
- isolate nested symbolic-shape token rendering from outer printer
configuration
---
include/tvm/ir/base_expr.h | 5 ++---
include/tvm/ir/expr.h | 5 +++++
include/tvm/relax/expr.h | 19 ++++++++++++++++
include/tvm/tirx/function.h | 25 ++++++++++++++++++++++
python/tvm/relax/op/memory/memory.py | 2 ++
python/tvm/relax/op/vm/vm.py | 2 ++
.../transform/lower_global_view_to_local_view.cc | 7 +++---
src/relax/script/printer/dependent_type.cc | 5 ++++-
.../relax/test_frontend_from_exported_program.py | 6 +++---
tests/python/relax/test_frontend_onnx.py | 2 ++
tests/python/relax/test_frontend_tflite.py | 8 ++++++-
tests/python/relax/test_utils.py | 2 +-
12 files changed, 75 insertions(+), 13 deletions(-)
diff --git a/include/tvm/ir/base_expr.h b/include/tvm/ir/base_expr.h
index 6d566bd5c9..a836d0410a 100644
--- a/include/tvm/ir/base_expr.h
+++ b/include/tvm/ir/base_expr.h
@@ -299,12 +299,11 @@ class ExprNode : public ffi::Object {
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
- // span and ty do not participate in structural equal and hash.
+ // span does not participate in structural equal and hash.
refl::ObjectDef<ExprNode>()
.def_ro("span", &ExprNode::span, refl::DefaultValue(Span()),
refl::AttachFieldFlag::SEqHashIgnore())
- .def_ro("ty", &ExprNode::ty, refl::DefaultValue(Type::Missing()),
- refl::AttachFieldFlag::SEqHashIgnore());
+ .def_ro("ty", &ExprNode::ty, refl::DefaultValue(Type::Missing()));
}
static constexpr TVMFFISEqHashKind _type_s_eq_hash_kind =
kTVMFFISEqHashKindTreeNode;
diff --git a/include/tvm/ir/expr.h b/include/tvm/ir/expr.h
index 7f1c84f50f..dd168a2462 100644
--- a/include/tvm/ir/expr.h
+++ b/include/tvm/ir/expr.h
@@ -276,6 +276,11 @@ class GlobalVarNode : public ExprNode {
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<GlobalVarNode>().def_ro("name_hint",
&GlobalVarNode::name_hint);
+ // A GlobalVar identifies a module-level symbol. Its type is derived from
the
+ // corresponding function definition and is not part of the symbol
identity.
+ refl::TypeAttrDef<GlobalVarNode>()
+ .def("__s_equal__", &GlobalVarNode::SEqual)
+ .def("__s_hash__", &GlobalVarNode::SHash);
}
bool SEqual(const GlobalVarNode* other,
diff --git a/include/tvm/relax/expr.h b/include/tvm/relax/expr.h
index 83e03c13e7..51b4ed43bd 100644
--- a/include/tvm/relax/expr.h
+++ b/include/tvm/relax/expr.h
@@ -501,6 +501,25 @@ class SeqExprNode : public ExprNode {
refl::ObjectDef<SeqExprNode>()
.def_ro("blocks", &SeqExprNode::blocks)
.def_ro("body", &SeqExprNode::body);
+ refl::TypeAttrDef<SeqExprNode>()
+ .def("__s_equal__", &SeqExprNode::SEqual)
+ .def("__s_hash__", &SeqExprNode::SHash);
+ }
+
+ bool SEqual(const SeqExprNode* other,
+ ffi::TypedFunction<bool(AnyView, AnyView, bool, AnyView)> equal)
const {
+ // Establish mappings for symbolic variables defined by bindings before
+ // comparing their uses in the SeqExpr result type and body.
+ return equal(blocks, other->blocks, false, "blocks") && equal(ty,
other->ty, false, "ty") &&
+ equal(body, other->body, false, "body");
+ }
+
+ int64_t SHash(int64_t init_hash, ffi::TypedFunction<int64_t(AnyView,
int64_t, bool)> hash) const {
+ int64_t hash_value = init_hash;
+ hash_value = hash(blocks, hash_value, false);
+ hash_value = hash(ty, hash_value, false);
+ hash_value = hash(body, hash_value, false);
+ return hash_value;
}
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("relax.expr.SeqExpr", SeqExprNode,
ExprNode);
};
diff --git a/include/tvm/tirx/function.h b/include/tvm/tirx/function.h
index e4e33f3576..a69b32a433 100644
--- a/include/tvm/tirx/function.h
+++ b/include/tvm/tirx/function.h
@@ -108,6 +108,31 @@ class PrimFuncNode : public BaseFuncNode {
.def_ro("ret_type", &PrimFuncNode::ret_type)
.def_ro("buffer_map", &PrimFuncNode::buffer_map)
.def_ro("body", &PrimFuncNode::body);
+ refl::TypeAttrDef<PrimFuncNode>()
+ .def("__s_equal__", &PrimFuncNode::SEqual)
+ .def("__s_hash__", &PrimFuncNode::SHash);
+ }
+
+ bool SEqual(const PrimFuncNode* other,
+ ffi::TypedFunction<bool(AnyView, AnyView, bool, AnyView)> equal)
const {
+ // `ty` is derived from the fields below. PrimFunc transformations update
+ // those source fields without maintaining this redundant cache eagerly.
+ // Remove this exception once all PrimFunc mutation paths recompute `ty`.
+ return equal(attrs, other->attrs, false, "attrs") &&
+ equal(params, other->params, true, "params") &&
+ equal(ret_type, other->ret_type, false, "ret_type") &&
+ equal(buffer_map, other->buffer_map, false, "buffer_map") &&
+ equal(body, other->body, false, "body");
+ }
+
+ int64_t SHash(int64_t init_hash, ffi::TypedFunction<int64_t(AnyView,
int64_t, bool)> hash) const {
+ int64_t hash_value = init_hash;
+ hash_value = hash(attrs, hash_value, false);
+ hash_value = hash(params, hash_value, true);
+ hash_value = hash(ret_type, hash_value, false);
+ hash_value = hash(buffer_map, hash_value, false);
+ hash_value = hash(body, hash_value, false);
+ return hash_value;
}
/*!
diff --git a/python/tvm/relax/op/memory/memory.py
b/python/tvm/relax/op/memory/memory.py
index 624d39a133..a1dec11069 100644
--- a/python/tvm/relax/op/memory/memory.py
+++ b/python/tvm/relax/op/memory/memory.py
@@ -98,6 +98,8 @@ def alloc_tensor(
shape = convert_to_expr(shape)
if isinstance(dtype, str):
dtype = DataTypeImm(dtype)
+ if isinstance(runtime_device_ind, int):
+ runtime_device_ind = prim_value(runtime_device_ind)
return _ffi_api.alloc_tensor(storage, offset, shape, dtype,
runtime_device_ind) # type: ignore
diff --git a/python/tvm/relax/op/vm/vm.py b/python/tvm/relax/op/vm/vm.py
index 0fc236d59b..afa9782e3d 100644
--- a/python/tvm/relax/op/vm/vm.py
+++ b/python/tvm/relax/op/vm/vm.py
@@ -98,6 +98,8 @@ def alloc_tensor(
shape = convert_to_expr(shape)
if isinstance(dtype, str):
dtype = DataTypeImm(dtype)
+ if isinstance(runtime_device_ind, int):
+ runtime_device_ind = prim_value(runtime_device_ind)
return _ffi_api.alloc_tensor(storage, offset, shape, dtype,
runtime_device_ind) # type: ignore
diff --git a/src/relax/distributed/transform/lower_global_view_to_local_view.cc
b/src/relax/distributed/transform/lower_global_view_to_local_view.cc
index f37c0d3863..c04970f631 100644
--- a/src/relax/distributed/transform/lower_global_view_to_local_view.cc
+++ b/src/relax/distributed/transform/lower_global_view_to_local_view.cc
@@ -166,10 +166,9 @@ class DistributedBufferCompactor : StmtExprMutator {
}
Stmt new_body = compactor(prim_func->body);
new_body = DistBufferReplacer::BufferReplace(new_body, replace_buffer_map);
- ffi::ObjectPtr<PrimFuncNode> new_func =
ffi::make_object<PrimFuncNode>(*prim_func.get());
- new_func->buffer_map = new_func_buffer_map;
- new_func->body = new_body;
- return std::make_tuple(PrimFunc(new_func), compactor.add_allreduce_kind_);
+ PrimFunc new_func(prim_func->params, new_body, prim_func->ret_type,
new_func_buffer_map,
+ prim_func->attrs, prim_func->span);
+ return std::make_tuple(new_func, compactor.add_allreduce_kind_);
}
private:
diff --git a/src/relax/script/printer/dependent_type.cc
b/src/relax/script/printer/dependent_type.cc
index d5c09cbcc7..c3b77223b5 100644
--- a/src/relax/script/printer/dependent_type.cc
+++ b/src/relax/script/printer/dependent_type.cc
@@ -54,7 +54,10 @@ ExprDoc PrintShapeVar(const PrimExpr& e, const AccessPath&
e_p, const IRDocsifie
}
// Step 3. Stringify the PrimExpr if func var exists
if (func_var_mode) {
- return LiteralDoc::Str(DocToPythonScript(expr_doc, d->cfg), e_p);
+ // This nested render is only converting a shape expression into one token.
+ // Give it an independent configuration so it cannot consume or emit the
+ // enclosing invocation's access-path diagnostics.
+ return LiteralDoc::Str(DocToPythonScript(expr_doc, PrinterConfig()), e_p);
}
return expr_doc;
}
diff --git a/tests/python/relax/test_frontend_from_exported_program.py
b/tests/python/relax/test_frontend_from_exported_program.py
index 1ee88ea846..afd53f1b74 100644
--- a/tests/python/relax/test_frontend_from_exported_program.py
+++ b/tests/python/relax/test_frontend_from_exported_program.py
@@ -6227,9 +6227,9 @@ def test_hamming_window():
with R.dataflow():
lv: R.Tensor((20,), dtype="float32") = R.hamming_window(
R.prim_value(20),
- R.prim_value(1),
- R.prim_value(T.float32(0.54000000000000004)),
- R.prim_value(T.float32(0.46000000000000002)),
+ R.prim_value(True),
+ R.prim_value(T.float64(0.54000000000000004)),
+ R.prim_value(T.float64(0.46000000000000002)),
dtype="float32",
)
gv: R.Tuple(R.Tensor((20,), dtype="float32")) = (lv,)
diff --git a/tests/python/relax/test_frontend_onnx.py
b/tests/python/relax/test_frontend_onnx.py
index f4dbac634a..9058cdf70a 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -3945,6 +3945,8 @@ def test_dynamic_shape_squeeze(axis):
axes = relax.Var("axes", relax.TensorType([1], "int64"))
gv = relax.Var("gv", tvm.ir.PrimType("int64"))
body = relax.SeqExpr([relax.DataflowBlock([relax.VarBinding(gv, a)])], gv)
+ # Match the importer boundary, where BlockBuilder populates the SeqExpr
result type.
+ body = relax.BlockBuilder().normalize(body)
expected_func = relax.Function([x, axes], body,
tvm.ir.PrimType("int64")).with_attrs(
{"num_input": 1, "global_symbol": "main"}
)
diff --git a/tests/python/relax/test_frontend_tflite.py
b/tests/python/relax/test_frontend_tflite.py
index 3397d08abb..c756227fe0 100644
--- a/tests/python/relax/test_frontend_tflite.py
+++ b/tests/python/relax/test_frontend_tflite.py
@@ -643,7 +643,13 @@ def test_unique():
R.func_attr({"num_input": 1})
with R.dataflow():
lv: R.Tuple(R.Tensor(dtype="int32", ndim=1),
R.Tensor(dtype="int64", ndim=1)) = (
- R.unique(x, R.prim_value(0), R.prim_value(0),
R.prim_value(1), R.prim_value(0))
+ R.unique(
+ x,
+ R.prim_value(False),
+ R.prim_value(False),
+ R.prim_value(True),
+ R.prim_value(False),
+ )
)
lv1: R.Tensor(dtype="int32", ndim=1) = lv[0]
lv2: R.Tensor(dtype="int64", ndim=1) = lv[1]
diff --git a/tests/python/relax/test_utils.py b/tests/python/relax/test_utils.py
index 9ee05b7fdd..b654edfc31 100644
--- a/tests/python/relax/test_utils.py
+++ b/tests/python/relax/test_utils.py
@@ -143,7 +143,7 @@ def test_assert_structural_equal_in_seqexpr():
with pytest.raises(
ValueError,
- match=re.escape("<root>.body.blocks[0].bindings[0].value.op"),
+ match=re.escape("<root>.ty.ret.shape.ty.values[0].value"),
):
assert_structural_equal(func_1, func_2)