Skip to content

Embedding Dual-Fixing into Probing - #3244

Open
Opt-Mucca wants to merge 50 commits into
latestfrom
probe-dual-fix
Open

Embedding Dual-Fixing into Probing#3244
Opt-Mucca wants to merge 50 commits into
latestfrom
probe-dual-fix

Conversation

@Opt-Mucca

@Opt-Mucca Opt-Mucca commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Description

This implement the technique from https://link.springer.com/article/10.1007/s12532-026-00336-z

Locally I've observed 1.5% time improvement and 8% node improvement across all-optimal. It affects roughly 50% of the instances that solve under both settings. That seems to be a bit better than the numbers that are reported in the paper, and I'd chalk it up to (1) I'm using a 30min time limit (2) I happen to have randomly dropped some unlucky MIPLIB instances (3) My implementation is more efficient (4) I fixed some errors in the reference implementation that were missing some fixings.

Edit: Forgot good old (5). I've written incorrect code.

It seems to affect sub-mips extremely heavily, so introduces a lot of noise on instances that are racing for a primal solution. I'd not trust my results without extra testing from @fwesselm (There's not that many instances where the size of the presolved problem changes, and even for those that do, it's usually only a minor reduction)

@fwesselm I tested this before the last commit, so there's a small chance I broke something (I will test it again over the weekend). I realised that copying the object each time a domain is copied doesn't make sense because it's only used in presolve, and now have tried to get a bit fancy with the copy constructors of HighsDomain. Hopefully this shaves off another small time improvement.

Checklist

  • I have read the contributing guidelines
  • This PR targets the latest branch
  • Tests are passing
  • Documentation was updated where relevant
  • This PR is not primarily AI-generated (per the AI contributions policy in CONTRIBUTING.md)

@Opt-Mucca
Opt-Mucca requested a review from fwesselm August 27, 2026 15:03
@Opt-Mucca

Copy link
Copy Markdown
Collaborator Author

@ZhaoWeiWang0319 feel like reviewing the code? I ended up changing a fair bit, but the core is the same.

@Opt-Mucca

Opt-Mucca commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

I clearly still have a bug given the failing tests..... Will fix that now

Edit: @ZhaoWeiWang0319 should now be good to review

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.13545% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.26%. Comparing base (f3a6291) to head (99b568b).
⚠️ Report is 84 commits behind head on latest.

Files with missing lines Patch % Lines
highs/mip/HighsDomain.cpp 98.15% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           latest    #3244      +/-   ##
==========================================
+ Coverage   73.21%   73.26%   +0.05%     
==========================================
  Files         445      445              
  Lines      107890   108385     +495     
  Branches    17278    17394     +116     
==========================================
+ Hits        78990    79408     +418     
- Misses      28624    28700      +76     
- Partials      276      277       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ZhaoWeiWang0319

Copy link
Copy Markdown
Contributor

I clearly still have a bug given the failing tests..... Will fix that now

Edit: @ZhaoWeiWang0319 should now be good to review

Thanks for handling this @Opt-Mucca ! There are indeed many helpful modifications, and I will go through them carefully tomorrow!

@ZhaoWeiWang0319

Copy link
Copy Markdown
Contributor

Hi @Opt-Mucca! Thank you very much for taking this forward and for the substantial cleanup and refactoring. I especially appreciate the work to separate the zero-cost fixings, rework how implications are handled, and resolve the merge, build, and test issues that came up along the way.

I have gone through the current implementations. I may be overlooking some details, but I have three questions/observations:

  1. Timing of the lifting-coefficient computation

In probing, HiGHS collect lifting opportunities for coefficients in inequalities. A row is recorded when it becomes redundant in probing, but the row slack used to form its lifting coefficient appears to be evaluated only at the end of the probing for a variable. By that point, the domain may already include disagreeing bound changes introduced by DualFixProbingPropagation through zero-cost fixings. As a result, the value returned by getRedundantRowValue() may include changes applied after the row was recorded.

Would it make sense to invoke storeLiftingOpportunities immediately before propagateZeroCosts() is called in HighsDomain.cpp, rather than at the end of probing in HighsImplications.cpp? My understanding is that this would prevent the later zero-cost fixings from affecting the lifting coefficient.

  1. Presolve behavior on neos-787933

Based our computational experiments while preparing the paper, dualfixProbing produced substantial presolve reductions on a nontrivial MIPLIB instance neos-787933, which could be used to test the functionality of the code. In particular, with my last implementation 0385e82, neos-787933 was reduced significantly during presolve. I no longer observe the same behavior on the current code.

I looked around but still did not know why.

  1. The safeUp and safeDown checks

I may be missing a subtlety here, but I wonder whether the safeUp and safeDown checks are still needed in the substitution condition in HighsImplications.cpp. Since zero-cost variables are now restricted to a consistent fixing direction across the two probing branches, my understanding is that this restriction should already make the resulting global reductions safe. Is that correct? If so, could these checks be removed to allow additional substitutions?

PS: I tried removing them locally so that more substitutions are allowed, but this alone did not recover the substantial reductions on neos-787933, so the behavior in point 2 may have a different cause.

These may simply be misunderstandings on my side, so please let me know if I have overlooked anything. Thanks again for all the work on this!!!

@Opt-Mucca

Copy link
Copy Markdown
Collaborator Author

@ZhaoWeiWang0319 Thank you so much for the review! Glad that you liked the the code-separation of the zero-cost fixings. Even if you're overlooking stuff this review was insanely helpful.

  1. Great spot. The row slack was definitely overly optimistic, and I'm hoping this wasn't 100% of the reason behind my good results..... I've followed your suggestion and invoked storeLiftingOpportunities directly before propagating any zero-cost fixings.

  2. I will look into this over the next few hours. I guess it wasn't just because I've removed the GDF reductions? I believe that there shouldn't really be any difference in the actual reductions otherwise, so this should point to an issue somewhere.

  3. I was too careful here and wasn't 100% sure that it's alright to store them. I think it needs a stronger result than just storing the direction across both probing values, i.e., needs to be consistent across all probed columns and all directions. zeroCostDirections_ is consistent across the entire process after double checking though, so I've now removed the whole dualSafe in the tentative implications. Another great spot!

@Opt-Mucca

Copy link
Copy Markdown
Collaborator Author

@ZhaoWeiWang0319 Found the reason for neos-787933!! In your original code you're copying the entire domchgstack and recording all binary implications within that stack. I was only storing the binary implications for comparison if some zero-cost fixing was applied to that branch. So the example:

  • Already know that x = 1 => y = 0 (stored in the clique table)
  • Conclude from a post zero-cost fixing propagation that x = 0 => y = 1.

I was not storing the first implication in dualFixProbingBinInds_ because it was (1) not necessarily from a branch with zero-cost fixing applied and (2) would have been removed from the simplified implication list as it's already stored in the clique table. I'll now run some tests with the new code to see if it makes any major performance difference. I'm a bit worried about the new overhead in always copying domchgstack.

Thank you for pointing out this instance! FYI: I'm 95% confident that all other solvers (including SCIP) solve this instance at the root node by replacing some of the knapsack rows by multiple setppc rows using information from the clique table. We've been unable to do this because of the presolve limitations on adding new rows, but this restriction has recently been lifted. It's cool that the instance is now "easily" solvable with a different technique (still takes a non-trivial amount of presolve time).

@ZhaoWeiWang0319

Copy link
Copy Markdown
Contributor

@ZhaoWeiWang0319 Found the reason for neos-787933!! In your original code you're copying the entire domchgstack and recording all binary implications within that stack. I was only storing the binary implications for comparison if some zero-cost fixing was applied to that branch. So the example:

* Already know that `x = 1 => y = 0` (stored in the clique table)

* Conclude from a post zero-cost fixing propagation that `x = 0 => y = 1`.

I was not storing the first implication in dualFixProbingBinInds_ because it was (1) not necessarily from a branch with zero-cost fixing applied and (2) would have been removed from the simplified implication list as it's already stored in the clique table. I'll now run some tests with the new code to see if it makes any major performance difference. I'm a bit worried about the new overhead in always copying domchgstack.

Thank you for pointing out this instance! FYI: I'm 95% confident that all other solvers (including SCIP) solve this instance at the root node by replacing some of the knapsack rows by multiple setppc rows using information from the clique table. We've been unable to do this because of the presolve limitations on adding new rows, but this restriction has recently been lifted. It's cool that the instance is now "easily" solvable with a different technique (still takes a non-trivial amount of presolve time).

@Opt-Mucca Glad that my observations are helpful !!

I also noticed some of the other updates - for example, no longer checking cliquetable.isFull() when fixing variables based on probing results involving zero-cost fixings. That change looks very reasonable to me.

The explanation for neos-787933 is particularly interesting. We hadn't realized how important copying the entire domchgstack was for this instance. You tiny example really helps clarify this. Huge thanks for working on this and everything else as well !

I think the code now is great, and I want to share more observations on neos-787933.

In fact, this instance was a bit of a surprise for us while preparing the paper. When we looked into it, we found many constraints of the form

\sum_{i=1}^n x_i <= n x_{n+1},

with all variables binary. In particular, fixing x_{n+1}=1 makes such a row redundant.

At the same time, this constraint can be disaggregated into n size-two clique constraints,

x_i + (1-x_{n+1}) <= 1,\qquad i=1,...,n,

so this seems closely related to the setppc/clique-table approach you mentioned. Being able to exploit this structure appears to be quite important for solving this instance effectively.

Another interesting observation on neos-787933 is that more than 80% of the total time - about 14s out of 18s in our run on my device - is spent in dual substitution (in HPresolve::dualFixing, one of the techniques in this PR: #2531; see also section 4.4 of Achterberg et al., Presolve Reductions in Mixed Integer Programming).

As discussed in Section 2.3 of our paper, once dual fixing is embedded into probing, dual substitution is theoretically dominated by our approach. In practice, the situation is slightly less clear because dual substitution scans all rows (meaning that currently HiGHS captures all possibilities for applying dual substitution), whereas probing generally does not probe every binary variable.

So - one possibility would be to disable dual substitution when this dual fixing is embedded into probing. Of course, whether that is beneficial overall would need to be evaluated on a larger test set.

@Opt-Mucca

Copy link
Copy Markdown
Collaborator Author

@fwesselm This should be ready for a review and for your larger testing framework. I'm getting a 1.5% improvement over three seeds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants