Repository navigation
Fix knapsack rational coefficient scaling #1754
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
Changes from all commits
e190192
5d796ba
1d545fe
440e3a0
df84179
9331432
ab73e71
5cf1edc
0c9ddb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2651,6 +2651,7 @@ i_t knapsack_generation_t<i_t, f_t>::generate_knapsack_cut( | |
| f_t objective_constant = 0.0; | ||
| std::vector<i_t> fixed_variables; | ||
| std::vector<f_t> fixed_values; | ||
| std::vector<f_t> fixed_weights; | ||
| const f_t x_tol = 1e-5; | ||
| for (i_t k = 0; k < knapsack_inequality.size(); k++) { | ||
| const i_t j = knapsack_inequality.index(k); | ||
|
|
@@ -2662,6 +2663,7 @@ i_t knapsack_generation_t<i_t, f_t>::generate_knapsack_cut( | |
| // if xstar_j is close to 0, then we can fix z to zero | ||
| fixed_variables.push_back(j); | ||
| fixed_values.push_back(0.0); | ||
| fixed_weights.push_back(knapsack_inequality.vector.x[k]); | ||
| seperation_rhs -= knapsack_inequality.vector.x[k]; | ||
| // No need to adjust the objective constant | ||
| continue; | ||
|
|
@@ -2670,6 +2672,7 @@ i_t knapsack_generation_t<i_t, f_t>::generate_knapsack_cut( | |
| // if xstar_j is close to 1, then we can fix z to 1 | ||
| fixed_variables.push_back(j); | ||
| fixed_values.push_back(1.0); | ||
| fixed_weights.push_back(knapsack_inequality.vector.x[k]); | ||
| // Note seperation rhs is unchanged | ||
| objective_constant += vj; | ||
| continue; | ||
|
|
@@ -2711,12 +2714,28 @@ i_t knapsack_generation_t<i_t, f_t>::generate_knapsack_cut( | |
| return -1; | ||
| } | ||
|
|
||
| i_t cover_size = 0; | ||
| i_t cover_size = 0; | ||
| f_t cover_weight = 0.0; | ||
| for (i_t k = 0; k < solution.size(); k++) { | ||
| if (solution[k] == 0.0) { cover_size++; } | ||
| if (solution[k] == 0.0) { | ||
| cover_size++; | ||
| cover_weight += weights[k]; | ||
| } | ||
| } | ||
| for (i_t k = 0; k < fixed_values.size(); k++) { | ||
| if (fixed_values[k] == 1.0) { cover_size++; } | ||
| if (fixed_values[k] == 1.0) { | ||
| cover_size++; | ||
| cover_weight += fixed_weights[k]; | ||
| } | ||
| } | ||
|
|
||
| // sum_{j in C} a_j > beta is what makes sum_{j in C} x_j <= |C| - 1 valid. The coefficients are | ||
| // integral here, so demand a full unit rather than letting rounding in the sums decide. | ||
| const bool is_cover = cover_weight >= knapsack_inequality.rhs + 1.0 - tol; | ||
| cuopt_assert(is_cover, "knapsack separation produced a set that is not a cover"); | ||
| if (!is_cover) { | ||
| restore_complemented(complemented_variables); | ||
| return -1; | ||
| } | ||
|
|
||
| cut.reserve(cover_size); | ||
|
|
@@ -2887,6 +2906,8 @@ void knapsack_generation_t<i_t, f_t>::minimal_cover_and_partition( | |
| } | ||
| } | ||
|
|
||
| cuopt_assert(cover_sum > beta, "minimal cover reduction dropped an item the cover needed"); | ||
|
|
||
| // Go through and correct cover_indicies and cover_coefficients | ||
| for (i_t k = 0; k < cover_coefficients.size();) { | ||
| if (cover_coefficients[k] == 0.0) { | ||
|
|
@@ -3220,7 +3241,10 @@ f_t knapsack_generation_t<i_t, f_t>::solve_knapsack_problem(const std::vector<f_ | |
|
|
||
| solution.assign(n, 0.0); | ||
|
|
||
| // dp(j, v) = minimum weight using first j items to get value v | ||
| // dp(j, v) = minimum weight using first j items to get value v. | ||
| // The weights are rounded up: rounding one down would let the dynamic programming algorithm | ||
| // return a set that violates the capacity, and the caller reads the complement of that set | ||
| // as a cover. | ||
| dense_matrix_t<i_t, i_t> dp(n + 1, sum_value + 1, INT_INF); | ||
| dense_matrix_t<i_t, uint8_t> take(n + 1, sum_value + 1, 0); | ||
| dp(0, 0) = 0; | ||
|
|
@@ -3233,8 +3257,7 @@ f_t knapsack_generation_t<i_t, f_t>::solve_knapsack_problem(const std::vector<f_ | |
|
|
||
| // Take item j-1 if possible | ||
| if (v >= scaled_values[j - 1]) { | ||
| i_t candidate = | ||
| dp(j - 1, v - scaled_values[j - 1]) + static_cast<i_t>(std::floor(weights[j - 1])); | ||
| i_t candidate = dp(j - 1, v - scaled_values[j - 1]) + std::ceil(weights[j - 1]); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: git show a42e4ca622982afff855c98c0d05fe54a4bb6299:cpp/src/cuts/cuts.cpp | sed -n '3220,3275p'
sed -n '3215,3305p' cpp/src/cuts/cuts.cpp
sed -n '2560,2590p' cpp/src/cuts/cuts.cppRepository: NVIDIA/cuopt Length of output: 5728 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- DP definitions and instantiation ---'
sed -n '3185,3275p' cpp/src/cuts/cuts.cpp
sed -n '6945,6990p' cpp/src/cuts/cuts.cpp
printf '%s\n' '--- rationalization definition and callers ---'
rg -n -A70 -B15 'rational_coefficients|knapsack_generation_t<' cpp/src/cuts/cuts.cpp
printf '%s\n' '--- relevant declarations/types ---'
rg -n -A25 -B15 'using i_t|typedef.*i_t|INT_INF|weights\[' cpp/src/cuts/cuts.cpp | head -220Repository: NVIDIA/cuopt Length of output: 43188 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- knapsack weights and DP call ---'
sed -n '2640,2735p' cpp/src/cuts/cuts.cpp
printf '%s\n' '--- rational approximation implementation ---'
rg -n -A90 -B20 'rational_approximation' cpp/src/cuts/cuts.cppRepository: NVIDIA/cuopt Length of output: 7867 Guard the Rationalization intends to produce integral coefficients, but the code allows the scaled product to land one ULP above its integer value. Therefore, Proposed fix- i_t candidate = dp(j - 1, v - scaled_values[j - 1]) + std::ceil(weights[j - 1]);
- if (candidate < dp(j, v)) {
- dp(j, v) = candidate;
- take(j, v) = 1;
+ const i_t prev = dp(j - 1, v - scaled_values[j - 1]);
+ const f_t w = std::ceil(weights[j - 1]);
+ if (prev < INT_INF && w < static_cast<f_t>(INT_INF - prev)) {
+ const i_t candidate = prev + static_cast<i_t>(w);
+ if (candidate < dp(j, v)) {
+ dp(j, v) = candidate;
+ take(j, v) = 1;
+ }
}🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I'm missing something. But that's the point of the fix. The ceiling increase the weight. So we ensure we satisfy the constraints, even if we produce a suboptimal solution. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
My concern is separate: the expression adds in floating point, then converts the result to The proposed guard keeps My original wording put too much emphasis on the intended weight increase. The conversion risk is largely pre-existing. ✏️ Learnings added
You are interacting with an AI system.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is risk of overflow in nearly every computation. What would happen in later code if we did exceed the maximum representable integer here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If execution continues with a corrupted, small
There are downstream safeguards:
Thus, a corrupted DP state does not automatically produce an invalid cut. The cover check provides a separate safeguard, although it cannot make undefined behavior safe. Your earlier clarification, also captured in the retrieved learning, is correct: 🧠 Learnings usedYou are interacting with an AI system. |
||
| if (candidate < dp(j, v)) { | ||
| dp(j, v) = candidate; | ||
| take(j, v) = 1; | ||
|
|
@@ -3260,6 +3283,15 @@ f_t knapsack_generation_t<i_t, f_t>::solve_knapsack_problem(const std::vector<f_ | |
| } | ||
| } | ||
|
|
||
| #ifdef ASSERT_MODE | ||
| f_t selected_weight = 0.0; | ||
| for (i_t j = 0; j < n; ++j) { | ||
| selected_weight += solution[j] * weights[j]; | ||
| } | ||
| cuopt_assert(selected_weight <= rhs + settings_.primal_tol, | ||
| "knapsack dynamic program returned a solution over capacity"); | ||
| #endif | ||
|
|
||
| objective = best_value * scale; | ||
| return objective; | ||
| } | ||
|
|
@@ -4918,6 +4950,7 @@ bool rational_coefficients(const std::vector<variable_type_t>& var_types, | |
| if (scalar < 0) { return false; } | ||
| if (std::abs(scalar) > 1000) { return false; } | ||
|
|
||
| // The scaled product can land an ulp off the integer it represents. | ||
| rational_inequality.scale(scalar); | ||
|
|
||
| return true; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.