Files
ortools-clone/ortools/pdlp/sharder.h
Laurent Perron d04d3798f1 [PDLP] sync
2025-01-27 13:50:49 +01:00

335 lines
15 KiB
C++

// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef PDLP_SHARDER_H_
#define PDLP_SHARDER_H_
#include <cstdint>
#include <functional>
#include <type_traits>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/log/check.h"
#include "ortools/pdlp/scheduler.h"
namespace operations_research::pdlp {
// This class represents a way to shard elements for multi-threading. The
// elements may be entries of a vector or the columns of a (column-major)
// matrix. The shards are selected to have roughly the same mass, where the mass
// of an entry depends on the constructor used. See the free functions below and
// in the .cc file for example usage.
class Sharder {
public:
// These are public aliases for convenience. They will change only if there
// are breaking changes in Eigen.
using ConstSparseColumnBlock = ::Eigen::Block<
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>,
/*BlockRows=*/Eigen::Dynamic, /*BlockCols=*/Eigen::Dynamic,
/*InnerPanel=*/true>;
using SparseColumnBlock =
::Eigen::Block<Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>,
/*BlockRows=*/Eigen::Dynamic, /*BlockCols=*/Eigen::Dynamic,
/*InnerPanel=*/true>;
// This class extracts a particular shard of vectors or matrices passed to it.
// See `ParallelForEachShard()`.
// Caution: Like `absl::Span`, `Shard::operator()` returns mutable or
// immutable views into the vector or matrix argument. The underlying object
// must outlive the view.
// Extra Caution: The const& arguments for the immutable views can bind to
// temporary objects, e.g., shard(3*a) will create a view into the "3*a"
// object that will be destroyed immediately after the shard is created.
class Shard {
public:
// Returns this shard of `vector`.
Eigen::VectorBlock<const Eigen::VectorXd> operator()(
const Eigen::VectorXd& vector) const {
CHECK_EQ(vector.size(), parent_.NumElements());
return vector.segment(parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_));
}
// Returns this shard of `vector` in mutable form.
Eigen::VectorBlock<Eigen::VectorXd> operator()(
Eigen::VectorXd& vector) const {
CHECK_EQ(vector.size(), parent_.NumElements());
return vector.segment(parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_));
}
// Returns this shard of `vector`.
Eigen::VectorBlock<const Eigen::VectorXd> operator()(
Eigen::VectorBlock<const Eigen::VectorXd> vector) const {
CHECK_EQ(vector.size(), parent_.NumElements());
return Eigen::VectorBlock<const Eigen::VectorXd>(
vector.nestedExpression(),
vector.startRow() + parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_));
}
// Returns this shard of `vector` in mutable form.
Eigen::VectorBlock<Eigen::VectorXd> operator()(
Eigen::VectorBlock<Eigen::VectorXd> vector) const {
CHECK_EQ(vector.size(), parent_.NumElements());
return Eigen::VectorBlock<Eigen::VectorXd>(
vector.nestedExpression(),
vector.startRow() + parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_));
}
// Returns this shard of `diag`. Note that the shard is a *square* diagonal
// matrix, not a block of columns of original length.
auto operator()(const Eigen::DiagonalMatrix<double, Eigen::Dynamic>& diag)
const -> decltype(diag.diagonal().segment(0, 0).asDiagonal()) {
CHECK_EQ(diag.diagonal().size(), parent_.NumElements());
return diag.diagonal()
.segment(parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_))
.asDiagonal();
}
// Returns this shard of the columns of `matrix`.
ConstSparseColumnBlock operator()(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix)
const {
CHECK_EQ(matrix.cols(), parent_.NumElements());
auto result = matrix.middleCols(parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_));
// This is a guard against implicit conversions, because the return type
// of `middleCols` is not 100% clear from the documentation.
static_assert(
std::is_same<decltype(result), ConstSparseColumnBlock>::value,
"The return type of middleCols changed!");
return result;
}
// Returns this shard of the columns of `matrix` in mutable form.
SparseColumnBlock operator()(
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix) const {
CHECK_EQ(matrix.cols(), parent_.NumElements());
auto result = matrix.middleCols(parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_));
// This is a guard against implicit conversions, because the return type
// of `middleCols` is not 100% clear from the documentation.
static_assert(std::is_same<decltype(result), SparseColumnBlock>::value,
"The return type of middleCols changed!");
return result;
}
// A non-negative identifier for this shard, less than `NumShards()` of the
// parent `Sharder`.
int Index() const { return shard_num_; }
private:
friend class Sharder;
Shard(int shard_num, const Sharder* parent)
: shard_num_(shard_num), parent_(*parent) {
CHECK_NE(parent, nullptr);
CHECK_GE(shard_num, 0);
CHECK_LT(shard_num, parent->NumShards());
}
const int shard_num_;
const Sharder& parent_;
};
// Creates a `Sharder` for problems with `num_elements` elements and mass of
// each element given by `element_mass`. Each shard will have roughly the same
// mass. The number of shards in the resulting `Sharder` will be approximately
// `num_shards` but may differ. The `scheduler` will be used for parallel
// operations executed by e.g. `ParallelForEachShard()`. The `scheduler` may
// be nullptr, which means work will be executed in the same thread. If
// `scheduler` is not nullptr, the underlying object is not owned and must
// outlive the `Sharder`.
Sharder(int64_t num_elements, int num_shards, Scheduler* scheduler,
const std::function<int64_t(int64_t)>& element_mass);
// Creates a `Sharder` for problems with `num_elements` elements and unit
// mass. This constructor exploits having all element mass equal to 1 to take
// time proportional to `num_shards` instead of `num_elements`. Also see the
// comments above the first constructor.
Sharder(int64_t num_elements, int num_shards, Scheduler* scheduler);
// Creates a `Sharder` for processing `matrix`. The elements correspond to
// columns of `matrix` and have mass linear in the number of non-zeros. Also
// see the comments above the first constructor.
Sharder(const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
int num_shards, Scheduler* scheduler)
: Sharder(matrix.cols(), num_shards, scheduler, [&matrix](int64_t col) {
return 1 + 1 * matrix.col(col).nonZeros();
}) {}
// Constructs a `Sharder` with the same scheduler as `other_sharder`, for
// problems with `num_elements` elements and unit mass. The number of shards
// will be approximately the same as that of `other_sharder`. Also see the
// comments on the first constructor.
Sharder(const Sharder& other_sharder, int64_t num_elements);
// `Sharder` may be moved, but not copied.
// Moved-from objects may be in an invalid state. The only methods that may be
// called on a moved-from object are the destructor or `operator=`.
Sharder(const Sharder& other) = delete;
Sharder& operator=(const Sharder& other) = delete;
Sharder(Sharder&& other) = default;
Sharder& operator=(Sharder&& other) = default;
int NumShards() const { return static_cast<int>(shard_starts_.size()) - 1; }
// The number of elements that are split into shards.
int64_t NumElements() const { return shard_starts_.back(); }
int64_t ShardSize(int shard) const {
CHECK_GE(shard, 0);
CHECK_LT(shard, NumShards());
return shard_starts_[shard + 1] - shard_starts_[shard];
}
int64_t ShardStart(int shard) const {
CHECK_GE(shard, 0);
CHECK_LT(shard, NumShards());
return shard_starts_[shard];
}
int64_t ShardMass(int shard) const {
CHECK_GE(shard, 0);
CHECK_LT(shard, NumShards());
return shard_masses_[shard];
}
// Runs `func` on each of the shards.
void ParallelForEachShard(
const std::function<void(const Shard&)>& func) const;
// Runs `func` on each of the shards and sums the results.
double ParallelSumOverShards(
const std::function<double(const Shard&)>& func) const;
// Runs `func` on each of the shards. Returns true iff all shards returned
// true.
bool ParallelTrueForAllShards(
const std::function<bool(const Shard&)>& func) const;
// Public for testing only.
const std::vector<int64_t>& ShardStartsForTesting() const {
return shard_starts_;
}
private:
// Size: `NumShards() + 1`. The first entry is 0 and the last entry is
// `NumElements()`. The entries are sorted in increasing order and are unique.
// Note that {0} is valid and indicates zero elements split into zero shards.
std::vector<int64_t> shard_starts_;
// Size: `NumShards()`. The mass of each shard.
std::vector<int64_t> shard_masses_;
// NOT owned. May be nullptr.
Scheduler* scheduler_;
};
// Like `matrix.transpose() * vector` but executed in parallel using `sharder`.
// The size of `sharder` must match the number of columns in `matrix`. To ensure
// good parallelization `matrix` should have (roughly) the same location of
// non-zeros as the `matrix` used when constructing `sharder`.
Eigen::VectorXd TransposedMatrixVectorProduct(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
const Eigen::VectorXd& vector, const Sharder& sharder);
////////////////////////////////////////////////////////////////////////////////
// The following functions use `sharder` to compute a vector operation in
// parallel. `sharder` should have the same size as the vector(s). For best
// performance `sharder` should have been created with the `Sharder(int64_t,
// int, Scheduler*)` constructor.
////////////////////////////////////////////////////////////////////////////////
// Like `dest.setZero(sharder.NumElements())`. Note that if `dest.size() !=
// sharder.NumElements()`, `dest` will be resized.
void SetZero(const Sharder& sharder, Eigen::VectorXd& dest);
// Like `VectorXd::Zero(sharder.NumElements())`.
Eigen::VectorXd ZeroVector(const Sharder& sharder);
// Like `VectorXd::Ones(sharder.NumElements())`.
Eigen::VectorXd OnesVector(const Sharder& sharder);
// Like `dest += scale * increment`.
void AddScaledVector(double scale, const Eigen::VectorXd& increment,
const Sharder& sharder, Eigen::VectorXd& dest);
// Like `dest = vec`. `dest` is resized if needed.
void AssignVector(const Eigen::VectorXd& vec, const Sharder& sharder,
Eigen::VectorXd& dest);
// Returns a copy of `vec`.
Eigen::VectorXd CloneVector(const Eigen::VectorXd& vec, const Sharder& sharder);
// Like `dest = dest.cwiseProduct(scale)`.
void CoefficientWiseProductInPlace(const Eigen::VectorXd& scale,
const Sharder& sharder,
Eigen::VectorXd& dest);
// Like `dest = dest.cwiseQuotient(scale)`.
void CoefficientWiseQuotientInPlace(const Eigen::VectorXd& scale,
const Sharder& sharder,
Eigen::VectorXd& dest);
// Like `v1.dot(v2)`.
double Dot(const Eigen::VectorXd& v1, const Eigen::VectorXd& v2,
const Sharder& sharder);
// Like `vector.lpNorm<Eigen::Infinity>()`, a.k.a. LInf norm.
double LInfNorm(const Eigen::VectorXd& vector, const Sharder& sharder);
// Like `vector.lpNorm<1>()`, a.k.a. L_1 norm.
double L1Norm(const Eigen::VectorXd& vector, const Sharder& sharder);
// Like `vector.squaredNorm()`.
double SquaredNorm(const Eigen::VectorXd& vector, const Sharder& sharder);
// Like `vector.norm()`.
double Norm(const Eigen::VectorXd& vector, const Sharder& sharder);
// Like `(vector1 - vector2).squaredNorm()`.
double SquaredDistance(const Eigen::VectorXd& vector1,
const Eigen::VectorXd& vector2, const Sharder& sharder);
// Like `(vector1 - vector2).norm()`.
double Distance(const Eigen::VectorXd& vector1, const Eigen::VectorXd& vector2,
const Sharder& sharder);
// `ScaledL1Norm` is omitted because it's not needed (yet).
// LInf norm of a rescaled vector, i.e.,
// `vector.cwiseProduct(scale).lpNorm<Eigen::Infinity>()`.
double ScaledLInfNorm(const Eigen::VectorXd& vector,
const Eigen::VectorXd& scale, const Sharder& sharder);
// Squared L2 norm of a rescaled vector, i.e.,
// `vector.cwiseProduct(scale).squaredNorm()`.
double ScaledSquaredNorm(const Eigen::VectorXd& vector,
const Eigen::VectorXd& scale, const Sharder& sharder);
// L2 norm of a rescaled vector, i.e., `vector.cwiseProduct(scale).norm()`.
double ScaledNorm(const Eigen::VectorXd& vector, const Eigen::VectorXd& scale,
const Sharder& sharder);
////////////////////////////////////////////////////////////////////////////////
// The functions below compute norms of the columns of a scaled matrix. The
// (i,j) entry of the scaled matrix equals `matrix[i,j] * row_scaling_vec[i]
// * col_scaling_vec[j]`. To ensure good parallelization `matrix` should have
// (roughly) the same location of non-zeros as the `matrix` used to construct
// `sharder`. The size of `sharder` must match the number of columns in
// `matrix`.
////////////////////////////////////////////////////////////////////////////////
// Computes the LInf norm of each column of a scaled `matrix`.
Eigen::VectorXd ScaledColLInfNorm(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
const Eigen::VectorXd& row_scaling_vec,
const Eigen::VectorXd& col_scaling_vec, const Sharder& sharder);
// Computes the L2 norm of each column of a scaled `matrix`.
Eigen::VectorXd ScaledColL2Norm(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
const Eigen::VectorXd& row_scaling_vec,
const Eigen::VectorXd& col_scaling_vec, const Sharder& sharder);
} // namespace operations_research::pdlp
#endif // PDLP_SHARDER_H_