Fix push/pop scopes leaking learned clauses into subsequent proofs#9221
Fix push/pop scopes leaking learned clauses into subsequent proofs#9221
Conversation
Add a 2-bit scope_lim field to the SAT clause struct to track the user scope level at which each learned clause was created. When user_pop is called, learned clauses that were added during the popped user scopes are removed. The root cause was that learned clauses accumulated during inner push/pop scopes were persisting after pop, polluting the clause database and biasing the search for subsequent proofs. - sat_clause.h: Add m_scope_lim:2 bitfield and set_scope_lim/scope_lim accessors - sat_clause.cpp: Initialize m_scope_lim=0 in constructor, copy in copy_clause - sat_solver.cpp (mk_nary_clause): Set scope_lim when adding learned clauses - sat_solver.cpp (user_pop): After gc_vars, remove learned clauses with scope_lim > new scope level (handles up to 3 levels of nesting precisely) Agent-Logs-Url: https://github.com/Z3Prover/z3/sessions/c23a0600-90dd-45a1-b64f-c286d041f373 Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Z3Prover/z3/sessions/c23a0600-90dd-45a1-b64f-c286d041f373 Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
|
@copilot create a benchmark where this makes a difference. Add a parameter to enable-disable this feature to allow testing. |
…d clause cleanup - src/params/sat_params.pyg: Add gc.learned_pop (bool, default true) parameter - src/sat/sat_config.h: Add m_gc_learned_pop field to config - src/sat/sat_config.cpp: Read gc.learned_pop from params - src/sat/sat_solver.cpp: Gate the user_pop cleanup on m_config.m_gc_learned_pop - src/test/sat_gc.cpp: New benchmark showing 6000+ clauses removed per run - src/test/main.cpp: Register tst_sat_gc in FOR_EACH_EXTRA_TEST - src/test/CMakeLists.txt: Add sat_gc.cpp to test sources Agent-Logs-Url: https://github.com/Z3Prover/z3/sessions/83cecb0d-37c4-4f96-a987-7d73ae8fabc3 Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
Added in commit a636fb3: Parameter: Benchmark ( The mechanism: conflicts during the inner-scope check that are derived entirely from outer-scope clauses (which carry no scope-guard literal) produce learned lemmas that survive |
|
the "fix" while plausible improvement for something has nothing to do with the original bug report. |
After
push()/check-sat()/pop(), learned clauses involving only outer-scope variables survivegc_vars()and persist in the clause database, polluting watch lists and biasing VSIDS activity scores for subsequent proofs outside the scope.Approach
Track the user scope level at clause creation using a new 2-bit
m_scope_limfield in theclausestruct (uses 2 previously unused bits in the existing packed bitfield). Duringuser_pop(), aftergc_vars()removes clauses containing pop'd variables, learned clauses withscope_lim > new_scope_levelare also removed.The field saturates at 3, so the cleanup is precise for up to 3 levels of nested push/pop; popping to scope level ≥3 cannot distinguish level-N from level-N+1 clauses, but the common cases (1–2 nesting levels) are handled exactly.
The cleanup is controlled by the new
sat.gc.learned_popparameter (defaulttrue), which can be set tofalseto disable the feature for testing or profiling.Changes
sat_clause.h: Addm_scope_lim:2bitfield andscope_lim()/set_scope_lim()accessors toclause.sat_clause.cpp: Initializem_scope_lim = 0in the constructor; copy it incopy_clause()so memory defragmentation preserves scope information.sat_solver.cpp(mk_nary_clause): Stamp each learned clause with the current user scope depth on creation.sat_solver.cpp(user_pop): Aftergc_vars(), sweepm_learnedand detach/delete any clause whosescope_limexceeds the new scope level, gated onm_config.m_gc_learned_pop.sat_params.pyg: Addgc.learned_pop(bool, defaulttrue) parameter.sat_config.h/sat_config.cpp: Addm_gc_learned_popfield and read it from params.src/test/sat_gc.cpp: New benchmark (tst_sat_gc) that uses a near-phase-transition 3-SAT formula (200 variables, ratio 4.5) with 3 push/pop cycles to demonstrate thatgc.learned_pop=trueremoves ~6000 stale learned clauses that would otherwise accumulate. Runs in ~0.1 s.