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 46bc8c6a38 [Fix][Arith] Give each materialized Z3 solver a private 
context (#20221)
46bc8c6a38 is described below

commit 46bc8c6a38e41b55fbc41f796e77773cb7b4f887
Author: Shushi Hong <[email protected]>
AuthorDate: Sat Aug 29 10:38:40 2026 -0400

    [Fix][Arith] Give each materialized Z3 solver a private context (#20221)
    
    This PR gives each lazily materialized Z3 solver a private context,
    preventing shared AST state from affecting results near the
    deterministic rlimit. The legacy Z3ContextScope APIs are retained as
    no-ops for compatibility.
    
    Co-authored-by: LeiWang1999 <[email protected]>
---
 include/tvm/arith/analyzer.h |  8 +++--
 python/tvm/arith/analyzer.py |  6 ++--
 src/arith/z3_prover.cc       | 83 ++++++++++++++++++--------------------------
 3 files changed, 42 insertions(+), 55 deletions(-)

diff --git a/include/tvm/arith/analyzer.h b/include/tvm/arith/analyzer.h
index dbde2cf0df..4d9c8b1365 100644
--- a/include/tvm/arith/analyzer.h
+++ b/include/tvm/arith/analyzer.h
@@ -599,14 +599,16 @@ class IntSetAnalyzer {
 /*!
  * \brief Enter a thread-local Z3 context scope.
  *
- * The outermost scope creates a fresh Z3 context. Nested scopes on the same
- * thread reuse that context so all Analyzers created during one compilation
- * can share it.
+ * Deprecated no-op. Every materialized Z3 solver owns a private context,
+ * which subsumes the per-compilation isolation these scopes provided; kept
+ * only until the remaining downstream call sites are removed.
  */
 TVM_DLL void EnterZ3ContextScope();
 
 /*!
  * \brief Exit the current thread-local Z3 context scope.
+ *
+ * Deprecated no-op, see EnterZ3ContextScope.
  */
 TVM_DLL void ExitZ3ContextScope();
 
diff --git a/python/tvm/arith/analyzer.py b/python/tvm/arith/analyzer.py
index 453d1aa33c..ad086518fb 100644
--- a/python/tvm/arith/analyzer.py
+++ b/python/tvm/arith/analyzer.py
@@ -118,10 +118,10 @@ class ConstraintScope:
 
 
 class Z3ContextScope:
-    """Share one fresh Z3 context across Analyzers created in this scope.
+    """Deprecated no-op retained for compatibility.
 
-    The outermost scope creates the context. Nested scopes on the same thread
-    reuse it, and exiting the outermost scope releases its ownership.
+    Every materialized Z3 solver now owns a private context, which provides
+    per-analyzer isolation without an explicit compilation scope.
     """
 
     def __enter__(self):
diff --git a/src/arith/z3_prover.cc b/src/arith/z3_prover.cc
index 083bc99381..49644c78ce 100644
--- a/src/arith/z3_prover.cc
+++ b/src/arith/z3_prover.cc
@@ -95,48 +95,17 @@ struct Namespace {
   }
 };
 
-struct Z3ContextState {
-  std::shared_ptr<z3::context> fallback_context;
-  std::shared_ptr<z3::context> scoped_context;
-  size_t scope_depth{0};
-};
-
-Z3ContextState& GetZ3ContextState() {
-  static thread_local Z3ContextState state;
-  return state;
-}
-
-std::shared_ptr<z3::context> GetCurrentZ3Context() {
-  auto& state = GetZ3ContextState();
-  if (state.scope_depth != 0) {
-    TVM_FFI_ICHECK(state.scoped_context != nullptr);
-    return state.scoped_context;
-  }
-  if (state.fallback_context == nullptr) {
-    state.fallback_context = std::make_shared<z3::context>();
-  }
-  return state.fallback_context;
-}
-
 }  // namespace
 
-void EnterZ3ContextScope() {
-  auto& state = GetZ3ContextState();
-  if (state.scope_depth == 0) {
-    state.scoped_context = std::make_shared<z3::context>();
-  }
-  ++state.scope_depth;
-}
+// Deprecated no-ops, kept only for callers of
+// arith.EnterZ3ContextScope / arith.ExitZ3ContextScope (Z3ContextScope in
+// Python). Shared per-compile Z3 contexts are gone: every materialized solver
+// owns a private context (see Z3Prover::Impl::Materialize), which subsumes the
+// per-compilation isolation these scopes provided. Remove together with the
+// remaining call sites.
+void EnterZ3ContextScope() {}
 
-void ExitZ3ContextScope() {
-  auto& state = GetZ3ContextState();
-  TVM_FFI_ICHECK_GT(state.scope_depth, 0U)
-      << "ExitZ3ContextScope called without a matching EnterZ3ContextScope";
-  --state.scope_depth;
-  if (state.scope_depth == 0) {
-    state.scoped_context.reset();
-  }
-}
+void ExitZ3ContextScope() {}
 
 class Z3Prover::Impl : ExprFunctor<z3::expr(const Expr&)> {
  public:
@@ -144,8 +113,10 @@ class Z3Prover::Impl : ExprFunctor<z3::expr(const Expr&)> {
   using Self = Z3Prover::Impl;
 
   AnalyzerObj* analyzer;
-  // Analyzers created in one compile scope share a context. Keeping the 
pointer
-  // on each prover also lets Analyzers and their clones outlive that scope.
+  /// @brief Z3 context owning every handle of this prover.
+  // Null until the solver materializes; Materialize creates a context private
+  // to this prover, which clones of a materialized prover then share so their
+  // copied handles stay valid.
   std::shared_ptr<z3::context> ctx;
 
   /// @brief Z3 solver instance
@@ -207,14 +178,14 @@ class Z3Prover::Impl : ExprFunctor<z3::expr(const Expr&)> 
{
     return result;
   }
 
-  Impl(AnalyzerObj* parent) : analyzer(parent), ctx(GetCurrentZ3Context()) {
-    // The solver is created lazily (see Materialize). Analyzers are
-    // constructed in large numbers as cheap scratch objects, and only a tiny
-    // fraction ever reaches the Z3 fallback of CanProve; creating the solver
-    // (and translating every Bind into z3 constraints) eagerly made every
-    // Analyzer pay Z3's initialization cost up front. Until the first
-    // operation that actually needs the solver, Bind/EnterConstraint only
-    // journal their arguments into scope_stack_.
+  Impl(AnalyzerObj* parent) : analyzer(parent) {
+    // The solver (and its context) is created lazily (see Materialize).
+    // Analyzers are constructed in large numbers as cheap scratch objects,
+    // and only a tiny fraction ever reaches the Z3 fallback of CanProve;
+    // creating the solver (and translating every Bind into Z3 constraints)
+    // eagerly made every Analyzer pay Z3's initialization cost up front.
+    // Until the first operation that actually needs the solver,
+    // Bind/EnterConstraint only journal their arguments into scope_stack_.
     scope_stack_.push_back({});
     scope_side_effects_.push_back({});
     // use rlimit, not timeout to ensure deterministic behavior
@@ -233,6 +204,14 @@ class Z3Prover::Impl : ExprFunctor<z3::expr(const Expr&)> {
   /// for these entries, so CanProve answers are unchanged.
   void Materialize() {
     if (solver) return;
+    // The solver gets its own context: verdicts under the deterministic
+    // rlimit budget depend on the context's accumulated AST state (ids feed
+    // the search heuristics), so sharing a context couples every solver's
+    // borderline answers to whichever analyzers happened to materialize
+    // before it. A private context makes each analyzer's answers a pure
+    // function of its own journal and query history. Unmaterialized provers
+    // hold no context (and no Z3 state) at all.
+    ctx = std::make_shared<z3::context>();
     solver.emplace(CreateSolver(*ctx));
     if (timeout_ms != UINT_MAX) {
       solver->set("timeout", timeout_ms);
@@ -410,6 +389,12 @@ class Z3Prover::Impl : ExprFunctor<z3::expr(const Expr&)> {
 
   /// @brief Binded
   /// @brief Bind a variable to a value or a range
+  /// A bind journaled inside a live constraint scope is dropped with that
+  /// scope. The eager path differed only cosmetically: it kept the var's memo
+  /// translation after the scope's solver frame (and with it the var's range
+  /// assertions) was popped, leaving an unconstrained placeholder --
+  /// observably the same as translating the var as a fresh free variable on
+  /// demand, which is what a later query on the journal does.
   void Bind(const Var& var, const PrimExpr& value, bool allow_override = 
false) {
     if (!IsZ3SupportedExpr(var.get())) return;
     scope_stack_.back().push_back(Scope{Scope::BindValue, var, value});

Reply via email to