From 7b831e1f6574c78fc63e7d54b72955f477b4c3d5 Mon Sep 17 00:00:00 2001 From: pisarev Date: Fri, 26 Jun 2026 23:31:16 +0300 Subject: [PATCH 1/2] [FFI] Make StructuralEqual functor compare tensor content The functor used skip_tensor_content=true while the StructuralHash functor hashes content. As the hash and key-equal of the constant de-duplication map they must agree, otherwise two distinct constants of equal shape and dtype can be merged on a bucket collision (platform dependent). Compare content so equal and hash stay consistent. --- include/tvm/ffi/extra/structural_equal.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/tvm/ffi/extra/structural_equal.h b/include/tvm/ffi/extra/structural_equal.h index ec960a85e..15876a569 100644 --- a/include/tvm/ffi/extra/structural_equal.h +++ b/include/tvm/ffi/extra/structural_equal.h @@ -69,7 +69,7 @@ class StructuralEqual { * \return True if the two Any values are structurally equal, false otherwise. */ TVM_FFI_INLINE bool operator()(const Any& lhs, const Any& rhs) const { - return Equal(lhs, rhs, false, true); + return Equal(lhs, rhs, false, false); } }; From 66b381bef253e63256da68f77494f0b1f65098c5 Mon Sep 17 00:00:00 2001 From: pisarev Date: Sat, 27 Jun 2026 01:09:33 +0300 Subject: [PATCH 2/2] [FFI] Add regression test for StructuralEqual tensor content --- tests/cpp/extra/test_structural_equal_hash.cc | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/cpp/extra/test_structural_equal_hash.cc b/tests/cpp/extra/test_structural_equal_hash.cc index 6ce380a49..7c36cbbcc 100644 --- a/tests/cpp/extra/test_structural_equal_hash.cc +++ b/tests/cpp/extra/test_structural_equal_hash.cc @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,21 @@ using namespace tvm::ffi; using namespace tvm::ffi::testing; namespace refl = tvm::ffi::reflection; +// CPU allocator and a helper to build a 1-D float32 tensor filled with one +// value, like test_tensor.cc does. +struct CPUNDAlloc { + void AllocData(DLTensor* tensor) { tensor->data = malloc(GetDataSize(*tensor)); } + void FreeData(DLTensor* tensor) { free(tensor->data); } +}; + +Tensor MakeFilledTensor(const Shape& shape, float value) { + Tensor t = Tensor::FromNDAlloc(CPUNDAlloc(), shape, DLDataType({kDLFloat, 32, 1}), + DLDevice({kDLCPU, 0})); + float* dst = reinterpret_cast(t.data_ptr()); + for (int64_t i = 0; i < t.numel(); ++i) dst[i] = value; + return t; +} + TEST(StructuralEqualHash, Array) { Array a = {1, 2, 3}; Array b = {1, 2, 3}; @@ -333,4 +349,28 @@ TEST(StructuralEqualHash, ArraySelfInsertProducesSnapshot) { EXPECT_EQ(StructuralHash()(arr), StructuralHash()(arr)); } +// Regression test for #645. StructuralHash hashes tensor content, so the +// StructuralEqual functor has to compare content too. Otherwise two distinct +// same-shape constants hash differently but compare equal, which breaks the +// constant de-dup map invariant and can silently merge different weights on a +// bucket collision. +TEST(StructuralEqualHash, TensorContent) { + Tensor zeros = MakeFilledTensor({4}, 0.0f); + Tensor ones = MakeFilledTensor({4}, 1.0f); + Tensor zeros_copy = MakeFilledTensor({4}, 0.0f); + + // Different content, same shape and dtype: not equal, and the hash differs. + EXPECT_FALSE(StructuralEqual()(zeros, ones)); + EXPECT_NE(StructuralHash()(zeros), StructuralHash()(ones)); + + // Identical content still compares equal and hashes equal, so real duplicates + // still get merged. + EXPECT_TRUE(StructuralEqual()(zeros, zeros_copy)); + EXPECT_EQ(StructuralHash()(zeros), StructuralHash()(zeros_copy)); + + // Skipping content is still available as an explicit opt-in. + EXPECT_TRUE(StructuralEqual::Equal(zeros, ones, /*map_free_vars=*/false, + /*skip_tensor_content=*/true)); +} + } // namespace