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-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new 1424286d [FFI] Make StructuralEqual functor compare tensor content 
(#646)
1424286d is described below

commit 1424286d49ab18784d3607030a79bcc644aabc86
Author: pisarev <[email protected]>
AuthorDate: Sun Jun 28 05:13:59 2026 +0300

    [FFI] Make StructuralEqual functor compare tensor content (#646)
    
    Fixes #645.
    
    The `StructuralEqual` functor calls `Equal(lhs, rhs, false,
    /*skip_tensor_content=*/true)`, while the `StructuralHash` functor
    hashes content (`skip_tensor_content = false`). The two are used
    together as the hash and key-equal of the constant de-duplication map in
    Relax VM codegen (`const_dedup_map_` in apache/tvm
    `src/relax/backend/vm/exec_builder.cc`). When hash and equal disagree,
    the map invariant `equal(a, b) => hash(a) == hash(b)` does not hold, so
    two distinct constants of equal shape and dtype get merged on a bucket
    collision and a later op reads the wrong constant.
    
    Which pair collides depends on the STL bucket count, so the same model
    produces wrong output under MSVC and correct output under libstdc++. The
    defect is latent on every platform.
    
    This change makes the functor compare content, so it agrees with the
    hash. Constants that are genuinely equal are still de-duplicated.
    
    Verified on a Windows build (MSVC 19.44, LLVM 18.1.8): after the change,
    YOLO11n det, YOLO11s cls, PP-OCRv5 det and PP-OCRv5 rec all match
    onnxruntime to within floating point; before, det and the PP-OCR models
    were off by 6 to 61 percent relative error.
    
    Full analysis in #645.
---
 include/tvm/ffi/extra/structural_equal.h      |  2 +-
 tests/cpp/extra/test_structural_equal_hash.cc | 43 +++++++++++++++++++++++++++
 2 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/include/tvm/ffi/extra/structural_equal.h 
b/include/tvm/ffi/extra/structural_equal.h
index ec960a85..15876a56 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);
   }
 };
 
diff --git a/tests/cpp/extra/test_structural_equal_hash.cc 
b/tests/cpp/extra/test_structural_equal_hash.cc
index 6ce380a4..c22d41ce 100644
--- a/tests/cpp/extra/test_structural_equal_hash.cc
+++ b/tests/cpp/extra/test_structural_equal_hash.cc
@@ -22,6 +22,7 @@
 #include <tvm/ffi/container/dict.h>
 #include <tvm/ffi/container/list.h>
 #include <tvm/ffi/container/map.h>
+#include <tvm/ffi/container/tensor.h>
 #include <tvm/ffi/extra/structural_equal.h>
 #include <tvm/ffi/extra/structural_hash.h>
 #include <tvm/ffi/object.h>
@@ -36,6 +37,24 @@ 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<float*>(t.data_ptr());
+  // dst points at GetDataSize(t) bytes (numel floats); the analyzer cannot 
infer that
+  // size through the allocator, so it wrongly flags this write as out of 
bounds.
+  // NOLINTNEXTLINE(clang-analyzer-security.ArrayBound)
+  for (int64_t i = 0; i < t.numel(); ++i) dst[i] = value;
+  return t;
+}
+
 TEST(StructuralEqualHash, Array) {
   Array<int> a = {1, 2, 3};
   Array<int> b = {1, 2, 3};
@@ -333,4 +352,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

Reply via email to