Skip to content

Three-tier nogood management system #213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

EmirDe
Copy link
Contributor

@EmirDe EmirDe commented Jun 24, 2025

I drafted a new nogood management strategy. The previous version was a "temporary" solution that stayed for too long; this version is supposed to be better.

There are a few changes:

  • This version implements the three-tier system for nogoods, this is the main change. This means that nogoods are partitioned into three categories (low-, mid-, and high-lbd), and the solver keeps them around for a different amount of time. Low-lbd are typically kept longer (most likely forever), high-lbd tier is temporary (similar to what we had before), whereas now there is also a mid-tier with lbds in between high and low, which is the main change compared to before.
  • The values for low-lbd is now 3, and high-lbd is 7 or greater. We might need to try out different values to see if there is any difference for CP benchmarks.
  • We now also delete some nogoods falsified at the root level. When cleaning up nogoods, if we happen to encounter a nogood falsified at the root level (one of its watchers is falsified at the root, or its cached predicate is falsified), then we will also remove that nogood. It could very well be the case that some nogoods are falsified, but we do not detect them in this pass (if we encounter that nogood later, its watchers will be updated so it will get removed later). We also do not simplify nogoods by removing true predicates, which could come later.

Can you please have a detailed look at the code, to see if something looks strange, e.g., does the code follow the comments?

In particular, there is an issue that surprises me, and where the tests fail. Have a look at the function are_watched_predicates_falsified_at_root_level. For some reason, when running the tests, getting a decision level for a predicate that is assigned fails - how is that possible?

@ImkoMarijnissen
Copy link
Contributor

In particular, there is an issue that surprises me, and where the tests fail. Have a look at the function are_watched_predicates_falsified_at_root_level. For some reason, when running the tests, getting a decision level for a predicate that is assigned fails - how is that possible?

Currently, the function get_decision_level_for_predicate only returns the decision level of satisfied predicates; negating the falsified predicates before retrieving the decision level fixes the issue with the test cases

.high_lbd
.iter()
.chain(self.learned_nogood_ids.mid_lbd.iter())
.chain(self.learned_nogood_ids.low_lbd.iter())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments above states that low-lbd nogoods do not get their activities updated. Should it be rescaled here then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, I removed the iterator over low_lbd nogoods.

/// Learned nogoods are partitioned into three categories: high-, mid-, and low-lbd nogoods.
/// Each tier has a predefined limit on the number of nogoods it may store.
/// If a tier has more nogoods than the limit prescribes, roughly half of the nogoods from the
/// tier are removed. High- and mid-tier nogoods remove the least active nogoods, whereas
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the sorting of low-tier nogoods based on the paper(s) or is this a design choice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is based on the paper listed right before this comment block. I tried to make this clear with the comments, can you have a look and let me know what was the issue?

context.is_predicate_falsified(watcher1)
&& context
.assignments
.get_decision_level_for_predicate(&watcher1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As stated in the comments, get_decision_level_for_predicate only works for satisfied predicates

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, fixed!

p.s. "As stated in the comments" -> which comments? For me, there were no comments for get_decision_level_for_predicate for the propagation context. I did add a comment now.

else {
self.learned_nogood_ids.low_lbd.push(*id);
false
fn are_watched_predicates_falsified_at_root_level(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name could be a bit more clear to indicate that it checks whether one of the watchers is falsified at the root

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Now called has_a_watched_predicate_falsified_at_root_level.

// We maintain a flag that signals if the process below will require another pass.
// This can happen when the cached predicate is falsified at the root level.
// Note that each nogood is watched twice.
// Since we do not know if this is the first or second watcher with this nogood,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be clarified a bit; what is meant by 'this', and the first or second watcher is a bit unclear. Maybe something like:

When we encounter a watcher where its cached predicate is falsified at the root, we mark it as deleted. However, we could have encountered the other watcher (with a different cached predicate) attached to this nogood prior to encountering this watcher with a falsified cached predicate ; to ensure that we delete this other watcher as well, we require another pass

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I updated the comment.

== 0
{
// Mark that a nogood has been deleted due to the cached predicate.
another_pass_needed = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we can retrieve what Predicate is being watched by the other watcher, perhaps we can do a more targeted deletion if this turns out to be a hotspot?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify? Here the nogood is deleted because of the cached predicate, and not a watched predicate.

Note that the first time we delete based on a watched predicate, the nogood gets marked as deleted, so it never tests its watched predicates again.

fn promote_mid_lbd_nogoods(&mut self) {
self.learned_nogood_ids.mid_lbd.retain(|id| {
// Still a mid-tier nogood?
if self.nogood_info[*id].lbd > self.parameters.lbd_low {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a > in this function but a >= in the promote_high_lbd_nogoods case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment in the if block. In short, the nogood is a mid-lbd nogood, so it is enough to test if it became a low-lbd nogood, otherwise it stays in the mid-lbd range.

@EmirDe EmirDe requested a review from ImkoMarijnissen June 25, 2025 12:32
@github-actions github-actions bot dismissed ImkoMarijnissen’s stale review June 25, 2025 12:32

Review re-requested

Copy link
Contributor

@maartenflippo maartenflippo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it looks good, but I would suggest making a refactoring. Can we separate a NogoodDatabase from the NogoodPropagator and have the database be a field on the propagator? At the moment, everything is in the propagator and it is extremely confusing to discern what is concerned with propagation and what is concerned with database management.

Comment on lines +620 to +622
/// Nogoods can move from higher tiers to lower tiers if the lbd drops sufficiently low, but
/// not the other way around. This is the main difference regarding this aspect from the paper
/// "Improving Implementation of SAT Competitions 2017–2019 Winners".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't low lbd go to high tiers? For completeness of this description we might want to include that. Right now, it seems a bit mysterious.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants