pisarev opened a new issue, #645:
URL: https://github.com/apache/tvm-ffi/issues/645

   ## Summary
   
   The functors `tvm::ffi::StructuralHash` and `tvm::ffi::StructuralEqual` 
disagree about tensor (NDArray) content:
   
   - `StructuralHash::operator()` hashes the tensor content 
(`skip_tensor_content = false`).
   - `StructuralEqual::operator()` ignores the tensor content 
(`skip_tensor_content = true`).
   
   They are used together as the hash and key-equal of the `std::unordered_map` 
that de-duplicates constants during Relax VM codegen (`const_dedup_map_` in 
`src/relax/backend/vm/exec_builder.cc`, in the apache/tvm repo). Because hash 
and equal disagree, the map's invariant `equal(a, b) => hash(a) == hash(b)` 
does not hold, and two different constants with the same shape and dtype can be 
merged into one. A downstream op (reshape/conv) then gets the wrong constant, 
and inference returns wrong numbers with no error or warning.
   
   The effect is platform dependent (I hit it on Windows/MSVC, while 
Linux/libstdc++ stays correct for the same model), but the defect itself is 
present on every platform.
   
   ## Root cause
   
   `include/tvm/ffi/extra/structural_equal.h`:
   
   ```cpp
   TVM_FFI_INLINE bool operator()(const Any& lhs, const Any& rhs) const {
     return Equal(lhs, rhs, false, true);   // skip_tensor_content = true
   }
   ```
   
   `include/tvm/ffi/extra/structural_hash.h`:
   
   ```cpp
   TVM_FFI_INLINE uint64_t operator()(const Any& value) const { return 
Hash(value); }
   // Hash(value, map_free_vars = false, skip_tensor_content = false) -> 
content is hashed
   ```
   
   For two tensors `a`, `b` with equal shape/dtype but different data:
   
   - `StructuralHash{}(a) != StructuralHash{}(b)` (content is hashed)
   - `StructuralEqual{}(a, b) == true` (content is ignored)
   
   In `unordered_map<Any, Index, StructuralHash, StructuralEqual>`, when 
`hash(a) % bucket_count == hash(b) % bucket_count` (a bucket collision, which 
becomes likely once the map holds a few dozen entries), `find(b)` scans that 
bucket, calls `StructuralEqual(a, b)`, gets `true`, and treats `b` as a 
duplicate of `a`. The emitted bytecode then references `a`'s constant index 
where it should reference `b`.
   
   ## Why it shows up on one platform and not another
   
   The hash values are the same on both platforms. The bucket count is not: 
MSVC's `std::unordered_map` and libstdc++'s grow their bucket counts on 
different schedules, so `hash % bucket_count` differs. A pair of constants that 
collides into the same bucket under one STL lands in separate buckets under the 
other. For a given model this is chance, so the same source produces wrong 
output under MSVC and correct output under libstdc++. A different model, more 
constants, or a different STL can surface it anywhere.
   
   ## Reproduction
   
   Functor level, with any two same-shape, different-data CPU tensors `a` 
(zeros) and `b` (ones), shape `(4,)` float32:
   
   ```cpp
   #include <tvm/ffi/extra/structural_equal.h>
   #include <tvm/ffi/extra/structural_hash.h>
   
   tvm::ffi::StructuralEqual eq;
   tvm::ffi::StructuralHash  h;
   CHECK(h(a) != h(b));       // holds: hash includes content
   CHECK(eq(a, b) == false);  // fails: equal returns true (content ignored)
   ```
   
   End to end: compile any Relax module for target `llvm` that has a few dozen 
distinct weight constants of the same shape (for example a YOLO11n detector 
imported with `relax.frontend.onnx.from_onnx`). Under MSVC the compiled module 
returns garbage (wrong class, large diff against onnxruntime); under libstdc++ 
it is correct. Diffing `ex.as_text()` of the two builds shows a single 
differing line: a `vm.builtin.reshape` reads constant `c[14]` (MSVC, the 
falsely merged one) instead of `c[59]` (libstdc++), and the MSVC executable has 
one fewer constant in its pool.
   
   ## Fix
   
   Make the `StructuralEqual` functor agree with the `StructuralHash` functor 
and compare content:
   
   ```cpp
   // structural_equal.h
   TVM_FFI_INLINE bool operator()(const Any& lhs, const Any& rhs) const {
     return Equal(lhs, rhs, false, false);   // was: false, true
   }
   ```
   
   With this change, four models that I convert through the ONNX frontend 
(YOLO11n det, YOLO11s cls, PP-OCRv5 det, PP-OCRv5 rec) match onnxruntime to 
within floating point on a Windows build (MSVC 19.44, LLVM 18.1.8); before the 
change, det and the PP-OCR models were off by 6 to 61 percent relative error. 
Constants that are genuinely equal are still de-duplicated (their content 
hashes equal and compares equal).
   
   If skipping tensor content during equality is wanted for a specific 
structural comparison, it can be requested through the static 
`StructuralEqual::Equal(..., /*skip_tensor_content=*/true)`. The functor used 
inside hash maps has to stay consistent with the `StructuralHash` functor.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to