speedup glop; better respect the time limit

This commit is contained in:
Laurent Perron
2025-02-05 18:11:16 +01:00
parent d3af4d76c9
commit 16687e6575
17 changed files with 131 additions and 66 deletions

View File

@@ -286,6 +286,7 @@ cc_library(
"//ortools/lp_data:lp_utils",
"//ortools/lp_data:scattered_vector",
"//ortools/util:stats",
"//ortools/util:time_limit",
],
)

View File

@@ -310,6 +310,10 @@ class BasisFactorization {
// Returns the number of updates since last refactorization.
int NumUpdates() const { return num_updates_; }
EntryIndex NumberOfEntriesInLU() const {
return lu_factorization_.NumberOfEntries();
}
private:
// Called by ForceRefactorization() or Refactorize() or Initialize().
Status ComputeFactorization();

View File

@@ -16,6 +16,7 @@
#include <cstdlib>
#include "ortools/lp_data/lp_utils.h"
#include "ortools/lp_data/permutation.h"
namespace operations_research {
namespace glop {
@@ -42,8 +43,8 @@ DenseColumn::ConstView DualEdgeNorms::GetEdgeSquaredNorms() {
void DualEdgeNorms::UpdateDataOnBasisPermutation(
const ColumnPermutation& col_perm) {
if (recompute_edge_squared_norms_) return;
ApplyColumnPermutationToRowIndexedVector(col_perm, &edge_squared_norms_,
&tmp_edge_squared_norms_);
ApplyColumnPermutationToRowIndexedVector(
col_perm.const_view(), &edge_squared_norms_, &tmp_edge_squared_norms_);
}
bool DualEdgeNorms::TestPrecision(RowIndex leaving_row,

View File

@@ -404,12 +404,14 @@ bool LuFactorization::LeftSolveLWithNonZeros(
// use a different algorithm.
ClearAndResizeVectorWithNonZeros(x->size(), result_before_permutation);
x->swap(result_before_permutation->values);
const auto input = result_before_permutation->values.const_view();
const auto inverse_row_perm = inverse_row_perm_.const_view();
if (nz->empty()) {
const RowIndex num_rows = inverse_row_perm_.size();
const RowIndex num_rows = inverse_row_perm.size();
for (RowIndex row(0); row < num_rows; ++row) {
const Fractional value = (*result_before_permutation)[row];
const Fractional value = input[row];
if (value != 0.0) {
const RowIndex permuted_row = inverse_row_perm_[row];
const RowIndex permuted_row = inverse_row_perm[row];
(*x)[permuted_row] = value;
}
}
@@ -417,8 +419,8 @@ bool LuFactorization::LeftSolveLWithNonZeros(
nz->swap(result_before_permutation->non_zeros);
nz->reserve(result_before_permutation->non_zeros.size());
for (const RowIndex row : result_before_permutation->non_zeros) {
const Fractional value = (*result_before_permutation)[row];
const RowIndex permuted_row = inverse_row_perm_[row];
const Fractional value = input[row];
const RowIndex permuted_row = inverse_row_perm[row];
(*x)[permuted_row] = value;
nz->push_back(permuted_row);
}

View File

@@ -63,7 +63,8 @@ Status Markowitz::ComputeRowAndColumnPermutation(
// Initialize residual_matrix_non_zero_ with the submatrix left after we
// removed the singleton and residual singleton columns.
residual_matrix_non_zero_.InitializeFromMatrixSubset(
basis_matrix, *row_perm, *col_perm, &singleton_column_, &singleton_row_);
basis_matrix, row_perm->const_view(), col_perm->const_view(),
&singleton_column_, &singleton_row_);
// Perform Gaussian elimination.
const int end_index = std::min(num_rows.value(), num_cols.value());
@@ -216,9 +217,9 @@ void Markowitz::ExtractSingletonColumns(
basis_matrix.num_rows().value());
}
bool Markowitz::IsResidualSingletonColumn(const ColumnView& column,
const RowPermutation& row_perm,
RowIndex* row) {
bool Markowitz::IsResidualSingletonColumn(
const ColumnView& column, StrictITISpan<RowIndex, const RowIndex> row_perm,
RowIndex* row) {
int residual_degree = 0;
for (const auto e : column) {
if (row_perm[e.row()] != kInvalidRow) continue;
@@ -238,7 +239,9 @@ void Markowitz::ExtractResidualSingletonColumns(
for (ColIndex col(0); col < num_cols; ++col) {
if ((*col_perm)[col] != kInvalidCol) continue;
const ColumnView column = basis_matrix.column(col);
if (!IsResidualSingletonColumn(column, *row_perm, &row)) continue;
if (!IsResidualSingletonColumn(column, row_perm->const_view(), &row)) {
continue;
}
(*col_perm)[col] = ColIndex(*index);
(*row_perm)[row] = RowIndex(*index);
lower_.AddDiagonalOnlyColumn(1.0);
@@ -569,8 +572,10 @@ void MatrixNonZeroPattern::Reset(RowIndex num_rows, ColIndex num_cols) {
}
void MatrixNonZeroPattern::InitializeFromMatrixSubset(
const CompactSparseMatrixView& basis_matrix, const RowPermutation& row_perm,
const ColumnPermutation& col_perm, std::vector<ColIndex>* singleton_columns,
const CompactSparseMatrixView& basis_matrix,
StrictITISpan<RowIndex, const RowIndex> row_perm,
StrictITISpan<ColIndex, const ColIndex> col_perm,
std::vector<ColIndex>* singleton_columns,
std::vector<RowIndex>* singleton_rows) {
const ColIndex num_cols = basis_matrix.num_cols();
const RowIndex num_rows = basis_matrix.num_rows();

View File

@@ -116,11 +116,12 @@ class MatrixNonZeroPattern {
// Resets the pattern to the one of the given matrix but only for the
// rows/columns whose given permutation is kInvalidRow or kInvalidCol.
// This also fills the singleton columns/rows with the corresponding entries.
void InitializeFromMatrixSubset(const CompactSparseMatrixView& basis_matrix,
const RowPermutation& row_perm,
const ColumnPermutation& col_perm,
std::vector<ColIndex>* singleton_columns,
std::vector<RowIndex>* singleton_rows);
void InitializeFromMatrixSubset(
const CompactSparseMatrixView& basis_matrix,
StrictITISpan<RowIndex, const RowIndex> row_perm,
StrictITISpan<ColIndex, const ColIndex> col_perm,
std::vector<ColIndex>* singleton_columns,
std::vector<RowIndex>* singleton_rows);
// Adds a non-zero entry to the matrix. There should be no duplicates.
void AddEntry(RowIndex row, ColIndex col);
@@ -377,8 +378,9 @@ class Markowitz {
// Helper function for determining if a column is a residual singleton column.
// If it is, RowIndex* row contains the index of the single residual edge.
bool IsResidualSingletonColumn(const ColumnView& column,
const RowPermutation& row_perm, RowIndex* row);
bool IsResidualSingletonColumn(
const ColumnView& column,
StrictITISpan<RowIndex, const RowIndex> row_perm, RowIndex* row);
// Returns the column of the current residual matrix with an index 'col' in
// the initial matrix. We compute it by solving a linear system with the

View File

@@ -156,16 +156,27 @@ void PrimalEdgeNorms::ComputeMatrixColumnNorms() {
void PrimalEdgeNorms::ComputeEdgeSquaredNorms() {
SCOPED_TIME_STAT(&stats_);
// time_limit_->LimitReached() can be costly sometimes, so we only do that
// if we feel this will be slow anyway.
const bool test_limit = (time_limit_ != nullptr) &&
basis_factorization_.NumberOfEntriesInLU() > 10'000;
// Since we will do a lot of inversions, it is better to be as efficient and
// precise as possible by refactorizing the basis.
DCHECK(basis_factorization_.IsRefactorized());
edge_squared_norms_.resize(compact_matrix_.num_cols(), 0.0);
edge_squared_norms_.resize(compact_matrix_.num_cols(), 1.0);
for (const ColIndex col : variables_info_.GetIsRelevantBitRow()) {
// Note the +1.0 in the squared norm for the component of the edge on the
// 'entering_col'.
edge_squared_norms_[col] = 1.0 + basis_factorization_.RightSolveSquaredNorm(
compact_matrix_.column(col));
// This operation can be costly, and we abort if we are stuck here.
// Note that we still mark edges as "recomputed" otherwise we can runs into
// some DCHECK before we actually abort the solve.
if (test_limit && time_limit_->LimitReached()) break;
}
recompute_edge_squared_norms_ = false;
}

View File

@@ -27,6 +27,7 @@
#include "ortools/lp_data/scattered_vector.h"
#include "ortools/lp_data/sparse.h"
#include "ortools/util/stats.h"
#include "ortools/util/time_limit.h"
namespace operations_research {
namespace glop {
@@ -146,6 +147,8 @@ class PrimalEdgeNorms {
return DeterministicTimeForFpOperations(num_operations_);
}
void SetTimeLimit(TimeLimit* time_limit) { time_limit_ = time_limit; }
private:
// Statistics about this class.
struct Stats : public StatsGroup {
@@ -192,6 +195,7 @@ class PrimalEdgeNorms {
const CompactSparseMatrix& compact_matrix_;
const VariablesInfo& variables_info_;
const BasisFactorization& basis_factorization_;
TimeLimit* time_limit_ = nullptr;
// Internal data.
GlopParameters parameters_;

View File

@@ -177,7 +177,7 @@ class RankOneUpdateFactorization {
}
// y->is_non_zero is always all false before and after this code.
DCHECK(IsAllFalse(y->is_non_zero));
DCHECK(y->is_non_zero.IsAllFalse());
y->RepopulateSparseMask();
bool use_dense = y->ShouldUseDenseIteration(hypersparse_ratio_);
for (int i = elementary_matrices_.size() - 1; i >= 0; --i) {
@@ -213,7 +213,7 @@ class RankOneUpdateFactorization {
}
// d->is_non_zero is always all false before and after this code.
DCHECK(IsAllFalse(d->is_non_zero));
DCHECK(d->is_non_zero.IsAllFalse());
d->RepopulateSparseMask();
bool use_dense = d->ShouldUseDenseIteration(hypersparse_ratio_);
const size_t end = elementary_matrices_.size();

View File

@@ -220,6 +220,7 @@ ABSL_MUST_USE_RESULT Status RevisedSimplex::SolveInternal(
[this, time_limit]() { AdvanceDeterministicTime(time_limit); });
SOLVER_LOG(logger_, "");
primal_edge_norms_.SetTimeLimit(time_limit);
if (logger_->LoggingIsEnabled()) {
DisplayBasicVariableStatistics();
}
@@ -2563,13 +2564,15 @@ void RevisedSimplex::PermuteBasis() {
if (col_perm.empty()) return;
// Permute basis_.
ApplyColumnPermutationToRowIndexedVector(col_perm, &basis_, &tmp_basis_);
ApplyColumnPermutationToRowIndexedVector(col_perm.const_view(), &basis_,
&tmp_basis_);
// Permute dual_pricing_vector_ if needed.
if (!dual_pricing_vector_.empty()) {
// TODO(user): We need to permute dual_prices_ too now, we recompute
// everything one each basis factorization, so this don't matter.
ApplyColumnPermutationToRowIndexedVector(col_perm, &dual_pricing_vector_,
ApplyColumnPermutationToRowIndexedVector(col_perm.const_view(),
&dual_pricing_vector_,
&tmp_dual_pricing_vector_);
}

View File

@@ -106,20 +106,21 @@ void UpdateRow::ComputeUpdateRow(RowIndex leaving_row) {
// small entries (no complexity changes).
const Fractional drop_tolerance = parameters_.drop_tolerance();
unit_row_left_inverse_filtered_non_zeros_.clear();
const auto view = transposed_matrix_.view();
const auto matrix_view = transposed_matrix_.view();
if (unit_row_left_inverse_.non_zeros.empty()) {
const ColIndex size = unit_row_left_inverse_.values.size();
const auto values = unit_row_left_inverse_.values.view();
for (ColIndex col(0); col < size; ++col) {
if (std::abs(unit_row_left_inverse_.values[col]) > drop_tolerance) {
if (std::abs(values[col]) > drop_tolerance) {
unit_row_left_inverse_filtered_non_zeros_.push_back(col);
num_row_wise_entries += view.ColumnNumEntries(col);
num_row_wise_entries += matrix_view.ColumnNumEntries(col);
}
}
} else {
for (const auto e : unit_row_left_inverse_) {
if (std::abs(e.coefficient()) > drop_tolerance) {
unit_row_left_inverse_filtered_non_zeros_.push_back(e.column());
num_row_wise_entries += view.ColumnNumEntries(e.column());
num_row_wise_entries += matrix_view.ColumnNumEntries(e.column());
}
}
}

View File

@@ -249,12 +249,17 @@ inline void PermuteWithScratchpad(
const IndexType size = input_output->size();
zero_scratchpad->swap(*input_output);
input_output->resize(size, 0.0);
for (IndexType index(0); index < size; ++index) {
const Fractional value = (*zero_scratchpad)[index];
// Caching the pointers help.
const Fractional* const input = zero_scratchpad->data();
const IndexType* const perm = permutation.data();
Fractional* const output = input_output->data();
for (int i = 0; i < size; ++i) {
DCHECK_GE(perm[i], 0);
DCHECK_LT(perm[i], size);
const Fractional value = input[i];
if (value != 0.0) {
const IndexType permuted_index(
permutation[PermutationIndexType(index.value())].value());
(*input_output)[permuted_index] = value;
output[perm[i].value()] = value;
}
}
zero_scratchpad->assign(size, 0.0);

View File

@@ -14,6 +14,9 @@
#ifndef OR_TOOLS_LP_DATA_PERMUTATION_H_
#define OR_TOOLS_LP_DATA_PERMUTATION_H_
#include <cstddef>
#include "absl/log/check.h"
#include "absl/random/random.h"
#include "ortools/base/strong_vector.h"
#include "ortools/lp_data/lp_types.h"
@@ -45,13 +48,13 @@ class Permutation {
public:
Permutation() : perm_() {}
explicit Permutation(IndexType size) : perm_(size.value(), IndexType(0)) {}
explicit Permutation(IndexType size) : perm_(size, IndexType(0)) {}
// This type is neither copyable nor movable.
Permutation(const Permutation&) = delete;
Permutation& operator=(const Permutation&) = delete;
IndexType size() const { return IndexType(perm_.size()); }
IndexType size() const { return perm_.size(); }
bool empty() const { return perm_.empty(); }
void clear() { perm_.clear(); }
@@ -60,9 +63,7 @@ class Permutation {
perm_.resize(size.value(), value);
}
void assign(IndexType size, IndexType value) {
perm_.assign(size.value(), value);
}
void assign(IndexType size, IndexType value) { perm_.assign(size, value); }
IndexType& operator[](IndexType i) { return perm_[i]; }
@@ -89,8 +90,16 @@ class Permutation {
// is -1. (Remembering hint: the signature of a swap (a 2-cycle) is -1.)
int ComputeSignature() const;
// For hot-loops it might be slighlty faster to cache the pointer and avoid
// bound checking on each [] access.
const IndexType* data() const { return perm_.data(); }
StrictITISpan<IndexType, const IndexType> const_view() const {
return perm_.view();
}
private:
util_intops::StrongVector<IndexType, IndexType> perm_;
StrictITIVector<IndexType, IndexType> perm_;
};
typedef Permutation<RowIndex> RowPermutation;
@@ -116,16 +125,21 @@ void ApplyInversePermutation(const Permutation<IndexType>& perm,
// row-indexed vector v.
template <typename RowIndexedVector>
void ApplyColumnPermutationToRowIndexedVector(
const Permutation<ColIndex>& col_perm, RowIndexedVector* v) {
RowIndexedVector temp_v = *v;
ApplyPermutation(col_perm, temp_v, v);
}
template <typename RowIndexedVector>
void ApplyColumnPermutationToRowIndexedVector(
const Permutation<ColIndex>& col_perm, RowIndexedVector* v,
StrictITISpan<ColIndex, const ColIndex> col_perm, RowIndexedVector* v,
RowIndexedVector* tmp) {
ApplyPermutation(col_perm, *v, tmp);
// Empty size means identity.
const int size = col_perm.size().value();
if (size == 0) return;
// Permute into tmp and swap.
DCHECK_EQ(size, v->size());
tmp->resize(RowIndex(size));
const auto* from = v->data();
auto* to = tmp->data();
for (int i = 0; i < size; ++i) {
to[col_perm[ColIndex(i)].value()] = from[i];
}
std::swap(*tmp, *v);
}
@@ -135,7 +149,7 @@ void ApplyColumnPermutationToRowIndexedVector(
template <typename IndexType>
void Permutation<IndexType>::PopulateFromInverse(const Permutation& inverse) {
const size_t size = inverse.perm_.size();
const IndexType size = inverse.perm_.size();
perm_.resize(size);
for (IndexType i(0); i < size; ++i) {
perm_[inverse[i]] = i;
@@ -144,7 +158,7 @@ void Permutation<IndexType>::PopulateFromInverse(const Permutation& inverse) {
template <typename IndexType>
void Permutation<IndexType>::PopulateFromIdentity() {
const size_t size = perm_.size();
const IndexType size = perm_.size();
perm_.resize(size, IndexType(0));
for (IndexType i(0); i < size; ++i) {
perm_[i] = i;
@@ -159,7 +173,7 @@ void Permutation<IndexType>::PopulateRandomly() {
template <typename IndexType>
bool Permutation<IndexType>::Check() const {
const size_t size = perm_.size();
const size_t size = perm_.size().value();
util_intops::StrongVector<IndexType, bool> visited(size, false);
for (IndexType i(0); i < size; ++i) {
if (perm_[i] < 0 || perm_[i] >= size) {
@@ -177,7 +191,7 @@ bool Permutation<IndexType>::Check() const {
template <typename IndexType>
int Permutation<IndexType>::ComputeSignature() const {
const size_t size = perm_.size();
const size_t size = perm_.size().value();
util_intops::StrongVector<IndexType, bool> visited(size);
DCHECK(Check());
int signature = 1;

View File

@@ -19,6 +19,7 @@
#include "absl/log/check.h"
#include "ortools/lp_data/lp_types.h"
#include "ortools/util/bitset.h"
namespace operations_research {
namespace glop {
@@ -61,7 +62,7 @@ struct ScatteredVector {
// Temporary vector used in some sparse computation on the ScatteredVector.
// True indicates a possible non-zero value. Note that its state is not always
// consistent.
StrictITIVector<Index, bool> is_non_zero;
Bitset64<Index> is_non_zero;
// In many cases there is a choice between treating the ScatteredVector as
// dense or as sparse. By default, dense algorithms are used when the
@@ -95,7 +96,7 @@ struct ScatteredVector {
void Add(Index index, Fractional value) {
values[index] += value;
if (!is_non_zero[index] && value != 0.0) {
is_non_zero[index] = true;
is_non_zero.Set(index);
non_zeros.push_back(index);
non_zeros_are_sorted = false;
}
@@ -128,20 +129,23 @@ struct ScatteredVector {
// Efficiently clears the is_non_zero vector.
void ClearSparseMask() {
if (ShouldUseDenseIteration()) {
is_non_zero.assign(values.size(), false);
is_non_zero.ClearAndResize(values.size());
} else {
is_non_zero.resize(values.size(), false);
is_non_zero.Resize(values.size());
for (const Index index : non_zeros) {
is_non_zero[index] = false;
is_non_zero.ClearBucket(index);
}
DCHECK(IsAllFalse(is_non_zero));
DCHECK(is_non_zero.IsAllFalse());
}
}
// Update the is_non_zero vector to be consistent with the non_zeros vector.
void RepopulateSparseMask() {
ClearSparseMask();
for (const Index index : non_zeros) is_non_zero[index] = true;
for (const Index index : non_zeros) {
// is_non_zero[index] = true;
is_non_zero.Set(index);
}
}
// If the proportion of non-zero entries is too large, clears the vector of

View File

@@ -1481,10 +1481,11 @@ void TriangularMatrix::ComputeRowsToConsiderInSortedOrder(
stored.Set(row);
}
const auto matrix_view = view();
const auto entry_rows = rows_.view();
for (int i = 0; i < non_zero_rows->size(); ++i) {
const RowIndex row = (*non_zero_rows)[i];
for (const EntryIndex index : Column(RowToColIndex(row))) {
for (const EntryIndex index : matrix_view.Column(RowToColIndex(row))) {
++num_ops;
const RowIndex entry_row = entry_rows[index];
if (!stored[entry_row]) {

View File

@@ -592,11 +592,13 @@ class Bitset64 {
// the higher order bits are assumed to be 0.
void Intersection(const Bitset64<IndexType>& other) {
const int min_size = std::min(data_.size(), other.data_.size());
uint64_t* const d = data_.data();
const uint64_t* const o = other.data_.data();
for (int i = 0; i < min_size; ++i) {
data_[i] &= other.data_[i];
d[i] &= o[i];
}
for (int i = min_size; i < data_.size(); ++i) {
data_[i] = 0;
d[i] = 0;
}
}
@@ -725,6 +727,11 @@ class Bitset64 {
return output;
}
bool IsAllFalse() const {
return std::all_of(data_.begin(), data_.end(),
[](uint64_t v) { return v == 0; });
}
private:
// Returns the value of the index type.
// This function is specialized below to work with IntType and int64_t.

View File

@@ -38,7 +38,7 @@
* Enables changing the behavior of the TimeLimit class to use -b usertime
* instead of \b walltime. This is mainly useful for benchmarks.
*/
OR_DLL ABSL_DECLARE_FLAG(bool, time_limit_use_usertime);
ABSL_DECLARE_FLAG(bool, time_limit_use_usertime);
namespace operations_research {
@@ -91,7 +91,7 @@ namespace operations_research {
*/
// TODO(user): The expression "deterministic time" should be replaced with
// "number of operations" to avoid confusion with "real" time.
class OR_DLL TimeLimit {
class TimeLimit {
public:
static const double kSafetyBufferSeconds; // See the .cc for the value.
static const int kHistorySize;