Skip to content

Commit de9a1a0

Browse files
authored
Fix race conditions in differential evolution (#1063)
Through a combination of silly mistakes, I missed a pile of race conditions in the OpenMP threading. Switch to C++ threading. Note that this change requires serial generation of trial vectors. Hopefully I can figure out to parallelize the generation of trial vectors to reduce the serial section a la Ahmdahl's law, while simultaneously keeping thread sanitizer happy.
1 parent 79b4015 commit de9a1a0

12 files changed

+621
-394
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ cmake-build-debug/*
2727
.cmake/*
2828
build.ninja
2929
.ninja*
30+
a.out

README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@ All the implementations are fully generic and support the use of arbitrary "real
3838

3939
These functions also provide the basis of support for the TR1 special functions.
4040

41-
### Root Finding and Function Minimisation
41+
### Root Finding
4242

4343
A comprehensive set of root-finding algorithms over the real line, both with derivatives and derivative free.
4444

45-
Also function minimisation via Brent's Method.
45+
### Optimization
46+
47+
Minimization of cost functions via Brent's method and differential evolution.
4648

4749
### Polynomials and Rational Functions
4850

doc/math.qbk

+3
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,9 @@ and as a CD ISBN 0-9504833-2-X 978-0-9504833-2-0, Classification 519.2-dc22.
724724
[mathpart root_finding Root Finding \& Minimization Algorithms]
725725
[include roots/roots_overview.qbk]
726726
[endmathpart] [/mathpart roots Root Finding Algorithms]
727+
[mathpart optimization Optimization]
728+
[include optimization/differential_evolution.qbk]
729+
[endmathpart] [/mathpart optimization Optimization]
727730

728731
[mathpart poly Polynomials and Rational Functions]
729732
[include internals/polynomial.qbk]

doc/roots/differential_evolution.qbk renamed to doc/optimization/differential_evolution.qbk

+13-7
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
1010
[heading Synopsis]
1111

1212
``
13-
#include <boost/math/tools/differential_evolution.hpp>
13+
#include <boost/math/optimization/differential_evolution.hpp>
1414

15-
namespace boost::math::tools {
15+
namespace boost::math::optimization {
1616

1717
template <typename ArgumentContainer> struct differential_evolution_parameters {
1818
using Real = typename ArgumentContainer::value_type;
1919
ArgumentContainer lower_bounds;
2020
ArgumentContainer upper_bounds;
21-
Real F = static_cast<Real>(0.65);
22-
double crossover_ratio = 0.5;
21+
Real mutation_factor = static_cast<Real>(0.65);
22+
double crossover_probability = 0.5;
2323
// Population in each generation:
2424
size_t NP = 200;
2525
size_t max_generations = 1000;
@@ -47,8 +47,8 @@ We justify this design choice by reference to the "No free lunch" theorem of Wol
4747

4848
`lower_bounds`: A container representing the lower bounds of the optimization space along each dimension. The `.size()` of the bounds should return the dimension of the problem.
4949
`upper_bounds`: A container representing the upper bounds of the optimization space along each dimension. It should have the same size of `lower_bounds`, and each element should be >= the corresponding element of `lower_bounds`.
50-
`F`: The scale factor controlling the rate at which the population evolves (default is 0.65).
51-
`crossover_ratio`: The crossover ratio determining the trade-off between exploration and exploitation (default is 0.5).
50+
`mutation_factor`: The F or scale factor controlling the rate at which the population evolves (default is 0.65).
51+
`crossover_probability`: The crossover probability determining the trade-off between exploration and exploitation (default is 0.5).
5252
`NP`: The population size (default is 200). Parallelization occurs over the population, so this should be "large".
5353
`max_generations`: The maximum number of generations for the optimization (default is 1000).
5454
`threads`: The number of threads to use for parallelization (default is the hardware concurrency). If the objective function is already multithreaded, then this should be set to 1 to prevent oversubscription.
@@ -64,7 +64,7 @@ The most robust way of decreasing the probability of getting stuck in a local mi
6464
template <typename ArgumentContainer, class Func, class URBG>
6565
ArgumentContainer differential_evolution(const Func cost_function,
6666
differential_evolution_parameters<ArgumentContainer> const &de_params,
67-
URBG &g,
67+
URBG &gen,
6868
std::invoke_result_t<Func, ArgumentContainer> value_to_reach = std::numeric_limits<std::invoke_result_t<Func, ArgumentContainer>>::quiet_NaN(),
6969
std::atomic<bool> *cancellation = nullptr,
7070
std::vector<std::pair<ArgumentContainer, std::invoke_result_t<Func, ArgumentContainer>>> *queries = nullptr,
@@ -102,6 +102,12 @@ Supported termination criteria are explicit requests for termination, value-to-r
102102
Price, Storn, and Lampinen, Section 2.8 also list population statistics and lack of accepted trials over many generations as sensible termination criteria.
103103
These could be supported if there is demand.
104104

105+
Parallelization with `std::thread` does have overhead-especially for very fast function calls.
106+
We found that the function call needs to be roughly a microsecond for unambigous parallelization wins.
107+
However, we have not provided conditional parallelization as computationally inexpensive cost functions are the exception; not the rule.
108+
If there is a clear use case for conditional parallelization (cheap cost function in very high dimensions is a good example),
109+
we can provide it.
110+
105111
[h4:references References]
106112

107113
* Price, Kenneth, Rainer M. Storn, and Jouni A. Lampinen. ['Differential evolution: a practical approach to global optimization.] Springer Science & Business Media, 2006.

doc/roots/roots_overview.qbk

-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ There are several fully-worked __root_finding_examples, including:
2121
[include quartic_roots.qbk]
2222
[include root_finding_examples.qbk]
2323
[include minima.qbk]
24-
[include differential_evolution.qbk]
2524
[include root_comparison.qbk]
2625

2726
[/ roots_overview.qbk

example/differential_evolution.cpp

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
* LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
66
*/
77
#include <iostream>
8-
#include <boost/math/tools/differential_evolution.hpp>
8+
#include <boost/math/optimization/differential_evolution.hpp>
99

10-
using boost::math::tools::differential_evolution_parameters;
11-
using boost::math::tools::differential_evolution;
10+
using boost::math::optimization::differential_evolution_parameters;
11+
using boost::math::optimization::differential_evolution;
1212

1313
double rosenbrock(std::vector<double> const & x) {
1414
double result = 0;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
* Copyright Nick Thompson, 2024
3+
* Use, modification and distribution are subject to the
4+
* Boost Software License, Version 1.0. (See accompanying file
5+
* LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6+
*/
7+
#ifndef BOOST_MATH_OPTIMIZATION_DETAIL_COMMON_HPP
8+
#define BOOST_MATH_OPTIMIZATION_DETAIL_COMMON_HPP
9+
#include <algorithm>
10+
#include <cmath>
11+
#include <limits>
12+
#include <sstream>
13+
#include <stdexcept>
14+
#include <random>
15+
16+
namespace boost::math::optimization::detail {
17+
18+
template <typename T, typename = void> struct has_resize : std::false_type {};
19+
20+
template <typename T>
21+
struct has_resize<T, std::void_t<decltype(std::declval<T>().resize(size_t{}))>> : std::true_type {};
22+
23+
template <typename T> constexpr bool has_resize_v = has_resize<T>::value;
24+
25+
template <typename ArgumentContainer>
26+
void validate_bounds(ArgumentContainer const &lower_bounds, ArgumentContainer const &upper_bounds) {
27+
using std::isfinite;
28+
std::ostringstream oss;
29+
if (lower_bounds.size() == 0) {
30+
oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
31+
oss << ": The dimension of the problem cannot be zero.";
32+
throw std::domain_error(oss.str());
33+
}
34+
if (upper_bounds.size() != lower_bounds.size()) {
35+
oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
36+
oss << ": There must be the same number of lower bounds as upper bounds, but given ";
37+
oss << upper_bounds.size() << " upper bounds, and " << lower_bounds.size() << " lower bounds.";
38+
throw std::domain_error(oss.str());
39+
}
40+
for (size_t i = 0; i < lower_bounds.size(); ++i) {
41+
auto lb = lower_bounds[i];
42+
auto ub = upper_bounds[i];
43+
if (lb > ub) {
44+
oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
45+
oss << ": The upper bound must be greater than or equal to the lower bound, but the upper bound is " << ub
46+
<< " and the lower is " << lb << ".";
47+
throw std::domain_error(oss.str());
48+
}
49+
if (!isfinite(lb)) {
50+
oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
51+
oss << ": The lower bound must be finite, but got " << lb << ".";
52+
oss << " For infinite bounds, emulate with std::numeric_limits<Real>::lower() or use a standard infinite->finite "
53+
"transform.";
54+
throw std::domain_error(oss.str());
55+
}
56+
if (!isfinite(ub)) {
57+
oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
58+
oss << ": The upper bound must be finite, but got " << ub << ".";
59+
oss << " For infinite bounds, emulate with std::numeric_limits<Real>::max() or use a standard infinite->finite "
60+
"transform.";
61+
throw std::domain_error(oss.str());
62+
}
63+
}
64+
}
65+
66+
template <typename ArgumentContainer, class URBG>
67+
std::vector<ArgumentContainer> random_initial_population(ArgumentContainer const &lower_bounds,
68+
ArgumentContainer const &upper_bounds,
69+
size_t initial_population_size, URBG &&gen) {
70+
using Real = typename ArgumentContainer::value_type;
71+
constexpr bool has_resize = detail::has_resize_v<ArgumentContainer>;
72+
std::vector<ArgumentContainer> population(initial_population_size);
73+
auto const dimension = lower_bounds.size();
74+
for (size_t i = 0; i < population.size(); ++i) {
75+
if constexpr (has_resize) {
76+
population[i].resize(dimension);
77+
} else {
78+
// Argument type must be known at compile-time; like std::array:
79+
if (population[i].size() != dimension) {
80+
std::ostringstream oss;
81+
oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
82+
oss << ": For containers which do not have resize, the default size must be the same as the dimension, ";
83+
oss << "but the default container size is " << population[i].size() << " and the dimension of the problem is "
84+
<< dimension << ".";
85+
oss << " The function argument container type is " << typeid(ArgumentContainer).name() << ".\n";
86+
throw std::runtime_error(oss.str());
87+
}
88+
}
89+
}
90+
91+
// Why don't we provide an option to initialize with (say) a Gaussian distribution?
92+
// > If the optimum's location is fairly well known,
93+
// > a Gaussian distribution may prove somewhat faster, although it
94+
// > may also increase the probability that the population will converge prematurely.
95+
// > In general, uniform distributions are preferred, since they best reflect
96+
// > the lack of knowledge about the optimum's location.
97+
// - Differential Evolution: A Practical Approach to Global Optimization
98+
// That said, scipy uses Latin Hypercube sampling and says self-avoiding sequences are preferable.
99+
// So this is something that could be investigated and potentially improved.
100+
using std::uniform_real_distribution;
101+
uniform_real_distribution<Real> dis(Real(0), Real(1));
102+
for (size_t i = 0; i < population.size(); ++i) {
103+
for (size_t j = 0; j < dimension; ++j) {
104+
auto const &lb = lower_bounds[j];
105+
auto const &ub = upper_bounds[j];
106+
population[i][j] = lb + dis(gen) * (ub - lb);
107+
}
108+
}
109+
110+
return population;
111+
}
112+
113+
template <typename ArgumentContainer>
114+
void validate_initial_guess(ArgumentContainer const &initial_guess, ArgumentContainer const &lower_bounds,
115+
ArgumentContainer const &upper_bounds) {
116+
std::ostringstream oss;
117+
auto const dimension = lower_bounds.size();
118+
if (initial_guess.size() != dimension) {
119+
oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
120+
oss << ": The initial guess must have the same dimensions as the problem,";
121+
oss << ", but the problem size is " << dimension << " and the initial guess has " << initial_guess.size()
122+
<< " elements.";
123+
throw std::domain_error(oss.str());
124+
}
125+
for (size_t i = 0; i < dimension; ++i) {
126+
auto lb = lower_bounds[i];
127+
auto ub = upper_bounds[i];
128+
if (!isfinite(initial_guess[i])) {
129+
oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
130+
oss << ": At index " << i << ", the initial guess is " << initial_guess[i]
131+
<< ", make sure all elements of the initial guess are finite.";
132+
throw std::domain_error(oss.str());
133+
}
134+
if (initial_guess[i] < lb || initial_guess[i] > ub) {
135+
oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
136+
oss << ": At index " << i << " the initial guess " << initial_guess[i] << " is not in the bounds [" << lb << ", "
137+
<< ub << "].";
138+
throw std::domain_error(oss.str());
139+
}
140+
}
141+
}
142+
143+
// Return indices corresponding to the minimum function values.
144+
template <typename Real> std::vector<size_t> best_indices(std::vector<Real> const &function_values) {
145+
using std::isnan;
146+
const size_t n = function_values.size();
147+
std::vector<size_t> indices(n);
148+
for (size_t i = 0; i < n; ++i) {
149+
indices[i] = i;
150+
}
151+
152+
std::sort(indices.begin(), indices.end(), [&](size_t a, size_t b) {
153+
if (isnan(function_values[a])) {
154+
return false;
155+
}
156+
if (isnan(function_values[b])) {
157+
return true;
158+
}
159+
return function_values[a] < function_values[b];
160+
});
161+
return indices;
162+
}
163+
164+
} // namespace boost::math::optimization::detail
165+
#endif

0 commit comments

Comments
 (0)