Ultra 0.9.4
Loading...
Searching...
No Matches
evolution.tcc
Go to the documentation of this file.
1
12
13#if !defined(ULTRA_EVOLUTION_H)
14# error "Don't include this file directly, include the specific .h instead"
15#endif
16
17#if !defined(ULTRA_EVOLUTION_TCC)
18#define ULTRA_EVOLUTION_TCC
19
20namespace internal
21{
22
23enum class phase {evolution, refinement};
24
25struct print_status
26{
27 phase run_phase {phase::evolution};
28
29 std::atomic<unsigned> planned_steps {0};
30 std::atomic<unsigned> completed_steps {0};
31
32 timer from_start {};
33 timer from_last_msg {};
34
35 std::size_t helpers {0};
36
37 void new_generation()
38 {
39 run_phase = phase::evolution;
40 planned_steps.store(0, std::memory_order_relaxed);
41 completed_steps.store(0, std::memory_order_relaxed);
42 }
43};
44
45class refinement_schedule
46{
47public:
48 explicit refinement_schedule(const parameters::refinement_parameters &rp)
49 : rp_(rp)
50 {
51 }
52
53 [[nodiscard]] bool ready(unsigned stagnation) const noexcept
54 {
55 return cooldown_left_ == 0 && stagnation >= rp_.stagnation_threshold;
56 }
57
58 void after_refinement_attempt(bool performed) noexcept
59 {
60 if (performed)
61 cooldown_left_ = rp_.cooldown;
62 }
63
64 void tick() noexcept
65 {
66 if (cooldown_left_)
67 --cooldown_left_;
68 }
69
70private:
71 const parameters::refinement_parameters &rp_;
72
73 unsigned cooldown_left_ {};
74};
75
81class task_group
82{
83public:
87 explicit task_group(std::size_t workers) : pool_(workers)
88 {
89 Expects(workers);
90 }
91
93 template<class F, class... Args>
94 requires std::same_as<std::invoke_result_t<F, Args...>, void>
95 void submit(F &&f, Args&&... args)
96 {
97 tasks_.push_back(pool_.submit(std::forward<F>(f),
98 std::forward<Args>(args)...));
99 }
100
102 void wait()
103 {
104 pool_.wait();
105 rethrow_exceptions();
106 }
107
116 template<class Rep, class Period>
117 [[nodiscard]] bool wait_for(const std::chrono::duration<Rep, Period> &timeout)
118 {
119 if (!pool_.wait_for(timeout))
120 return false;
121
122 rethrow_exceptions();
123 return true;
124 }
125
126private:
127 void rethrow_exceptions()
128 {
129 std::exception_ptr first_exception;
130
131 for (auto &task : tasks_)
132 try
133 {
134 task.get();
135 }
136 catch (...)
137 {
138 if (!first_exception)
139 first_exception = std::current_exception();
140 }
141
142 tasks_.clear();
143
144 if (first_exception)
145 std::rethrow_exception(first_exception);
146 }
147
148 ultra::thread_pool pool_;
149 std::vector<std::future<void>> tasks_ {};
150};
151
165template<Evaluator E>
166[[nodiscard]] std::size_t set_evaluator_parallelism(
167 std::size_t allocated, std::size_t min_samples_per_eval_thread, E &eva)
168{
169 if constexpr (requires { eva.set_parallel_evaluation(0, 0); })
170 {
171 const auto capacity(hardware_threads());
172 const auto helpers(allocated && allocated < capacity ?
173 (capacity - allocated) / allocated : 0);
174
175 ultraDEBUG("Configure evaluator's parallelism to {}", helpers);
176 if (eva.set_parallel_evaluation(helpers, min_samples_per_eval_thread))
177 return helpers;
178 }
179
180 return 0;
181}
182
183} // namespace internal
184
194template<Evaluator E>
195evolution<E>::evolution(const problem &prob, E &eva) : pop_(prob), eva_(eva)
196{
197 Ensures(is_valid());
198
199 ultraDEBUG("Creating a new instance of evolution class");
200}
201
212template<Evaluator E>
213bool evolution<E>::stop_condition() const
214{
215 const auto planned_generations(pop_.problem().params.evolution.generations);
216 Expects(planned_generations);
217
218 // Check the number of generations.
219 if (sum_.generation >= planned_generations)
220 return true;
221
222 if (console.user_stop())
223 return true;
224
225 return external_stop_source_.stop_requested();
226}
227
228template<Evaluator E>
229void evolution<E>::print(message m, internal::print_status &ps) const
230{
231 if (!emit_messages_ || log::reporting_level > log::lPAROUT)
232 return;
233
234 const std::string tags(tag_.empty() ? tag_ : "[" + tag_ + "] ");
235
236 const std::string_view str_phase(ps.run_phase == internal::phase::evolution
237 ? "evolution" : "refinement");
238
239 const auto elapsed(ps.from_start.elapsed());
240
241 if (m == message::summary)
242 {
243 const auto fitness(internal::streamed(sum_.best().fit));
244
245 ultraPAROUT("{}{:>8}{:>8}{:>12}:{:>13}", tags,
246 lexical_cast<std::string>(elapsed), sum_.generation,
247 str_phase, fitness);
248 }
249 else // message::status
250 {
251 std::string perc;
252
253 const auto planned(ps.planned_steps.load(std::memory_order_relaxed));
254 if (planned)
255 {
256 const auto completed(ps.completed_steps.load(std::memory_order_relaxed));
257 perc = std::to_string(std::uintmax_t(100) * completed / planned) + "%";
258 }
259
260 std::string line;
261 if (log::reporting_level == log::lPAROUT)
262 {
263 static constexpr std::string_view chrs("|/-\\");
264
265 line = std::format("{}{} {} {}",
266 tags,
267 chrs[elapsed.count() % chrs.size()],
268 str_phase,
269 perc);
270 }
271 else if (log::reporting_level <= log::lSTDOUT)
272 {
273 const auto seconds(
274 std::max(std::chrono::duration_cast<std::chrono::seconds>(elapsed),
275 1s).count());
276
277 line = std::format("{} gen {} {} {}",
278 lexical_cast<std::string>(elapsed),
279 sum_.generation,
280 str_phase,
281 perc);
282
283 if (sum_.generation)
284 {
285 const double gph(3600.0 * sum_.generation / seconds);
286 line += std::format(" [{}L", pop_.layers());
287 if (ps.helpers)
288 line += std::format(" x {}T", 1 + ps.helpers);
289 line += std::format(" | {0:.{1}f}gph]", gph, gph > 2.0 ? 0 : 1);
290 }
291 }
292
293 console.status_line(line);
294 }
295
296 if (ps.run_phase == internal::phase::evolution)
297 ps.from_last_msg.restart();
298}
299
311template<Evaluator E>
312evolution<E> &evolution<E>::logger(search_log &sl)
313{
314 search_log_ = &sl;
315 return *this;
316}
317
318template<Evaluator E>
319evolution<E> &evolution<E>::refinement(refinement_callback_t f)
320{
321 refinement_callback_ = std::move(f);
322 return *this;
323}
324
325template<Evaluator E>
326evolution<E> &evolution<E>::messages(bool m) noexcept
327{
328 emit_messages_ = m;
329 return *this;
330}
331
341template<Evaluator E>
342evolution<E> &evolution<E>::tag(const std::string &t)
343{
344 tag_ = t;
345 return *this;
346}
347
358template<Evaluator E>
359evolution<E> &evolution<E>::shake_function(shake_function_callback_t f)
360{
361 shake_ = std::move(f);
362 return *this;
363}
364
371template<Evaluator E>
372evolution<E> &evolution<E>::after_generation(after_generation_callback_t f)
373{
374 after_generation_callback_ = std::move(f);
375 return *this;
376}
377
378template<Evaluator E>
379evolution<E> &evolution<E>::on_new_best(on_new_best_callback_t f)
380{
381 sum_.on_new_best(std::move(f));
382 return *this;
383}
384
394template<Evaluator E>
395evolution<E> &evolution<E>::stop_source(std::stop_source ss)
396{
397 external_stop_source_ = ss;
398 return *this;
399}
400
415template<Evaluator E>
416template<Population P>
417auto evolution<E>::refinement_tournament(const P &ref_pop)
418{
419 const auto &params(pop_.problem().params);
420 const auto t_size(params.refinement.tournament_size
421 ? params.refinement.tournament_size
422 : params.evolution.tournament_size);
423 assert(t_size > 0);
425
426 auto best_coord(random::coord(ref_pop));
427 auto best_fit(eva_(ref_pop[best_coord]));
428
429 for (std::size_t i(1); i < t_size; ++i)
430 {
431 const auto c(random::coord(ref_pop));
432 const auto fit(eva_(ref_pop[c]));
433
434 if (fit > best_fit)
435 {
436 best_coord = c;
437 best_fit = fit;
438 }
439 }
440
441 return best_coord;
442}
443
455template<Evaluator E>
456template<Population P>
457bool evolution<E>::perform_refinement(P &ref_pop, internal::print_status &ps)
458{
459 if (!ref_pop.size())
460 return false;
461
462 const auto refinement_fraction(pop_.problem().params.refinement.fraction);
463 Expects(in_0_1(refinement_fraction));
464
465 if (issmall(refinement_fraction))
466 return false;
467
468 // ********* Select refinement candidates *********
469 const auto base_n(
470 static_cast<std::size_t>(refinement_fraction * ref_pop.size()));
471 const std::size_t n(std::max(base_n, 1uz));
472
473 struct refinement_candidate
474 {
475 typename P::coord coord;
476 individual_t initial;
478 };
479
480 std::vector<refinement_candidate> candidates;
481 candidates.reserve(n);
482
483 std::set<typename P::coord> seen;
484
485 for (std::size_t i(0); i < n; ++i)
486 {
487 const auto coord(refinement_tournament(ref_pop));
488
489 if (seen.insert(coord).second)
490 candidates.push_back({coord, ref_pop[coord], {}});
491 }
492
493 if (candidates.empty())
494 return false;
495
496 // ********* Refine candidates in parallel *********
497 ps.run_phase = internal::phase::refinement;
498 ps.planned_steps.store(static_cast<unsigned>(candidates.size()),
499 std::memory_order_relaxed);
500 ps.completed_steps.store(0, std::memory_order_relaxed);
501
502 const auto threads(std::min(candidates.size(), hardware_threads()));
503
505 threads, pop_.problem().params.min_samples_per_eval_thread, eva_);
506
507 internal::task_group tasks(threads);
508
509 for (std::size_t i(0); i < candidates.size(); ++i)
510 tasks.submit(
511 [&, i]
512 {
513 // Suppress backend output here: the evolution loop reports refinement
514 // progress and multiple backend instances may run concurrently.
515 refiner optimiser(pop_.problem(), false);
516
517 auto ind(candidates[i].initial);
518
519 const auto fit(
520 optimiser.optimise(ind, eva_, refinement_callback_));
521
522 if (fit)
523 candidates[i].refined = scored_individual(ind, *fit);
524
525 ps.completed_steps.fetch_add(1, std::memory_order_relaxed);
526 print(message::status, ps);
527 });
528
529 tasks.wait();
530
531 // Commit refinement results.
532 for (const auto &result : candidates)
533 if (!result.refined.empty())
534 {
535 if (sum_.update_if_better(result.refined))
536 print(message::summary, ps);
537
538 ref_pop[result.coord] = result.refined.ind;
539 }
540
541 // Restore evolution phase and population-layer parallelism.
542 ps.run_phase = internal::phase::evolution;
544 pop_.layers(), pop_.problem().params.min_samples_per_eval_thread,
545 eva_);
546
547 return true;
548}
549
571template<Evaluator E>
572template<template<class> class ES>
574 typename evolution<E>::fitness_t> evolution<E>::run()
575{
576 Expects(sum_.generation == 0);
577
578 const auto &params(pop_.problem().params);
579
580 if (params.needs_init())
581 throw std::logic_error("Parameters still contain auto-tune values. "
582 "Call parameters::init() (or use src::search) "
583 "before evolution::run().");
584
585 using namespace std::chrono_literals;
586
587 std::stop_source source;
588
589 // An external stop request immediately requests cancellation on the local
590 // source.
591 std::stop_callback external_stop_callback(
592 external_stop_source_.get_token(),
593 [&source] { source.request_stop(); });
594
595 if (source.stop_requested())
596 return sum_;
597
598 ES<E> strategy(pop_.problem(), eva_);
599
600 // Last observed best (for progress reporting).
601 scored_individual progress_best(sum_.best());
602
603 internal::print_status ps;
604 internal::refinement_schedule ref_sched(params.refinement);
605
606 // Asynchronous population update: each newly generated offspring can replace
607 // an individual of the current population (aka steady state population).
608 // Asynchronous update permits new individual to contribute to the evolution
609 // immediately and can speed up the convergence.
610 const auto evolve_subpop(
611 [&, stop_token = source.get_token()](auto subpop_it)
612 {
613 auto evolve(strategy.operations(pop_, subpop_it,
614 sum_.starting_status()));
615
616 // We must use `safe_size()` because other threads might migrate
617 // individuals in this subpopulation.
618 auto cycles(subpop_it->safe_size());
619
620 ps.planned_steps.fetch_add(cycles, std::memory_order_relaxed);
621
622 while (cycles--)
623 if (!stop_token.stop_requested())
624 {
625 evolve();
626 ps.completed_steps.fetch_add(1, std::memory_order_relaxed);
627 }
628 });
629
630 const auto print_and_update_if_better(
631 [&](auto candidate)
632 {
633 if (progress_best < candidate)
634 {
635 progress_best = candidate;
636 print(message::summary, ps);
637 return true;
638 }
639
640 return false;
641 });
642
643 ultraDEBUG("Calling evolution_strategy init method");
644 strategy.init(pop_); // strategy-specific customisation point
645
646 internal::task_group tasks(strategy.max_parallelism(pop_));
647
648 while (!source.stop_requested() && !stop_condition())
649 {
650 ps.new_generation();
651
652 if (shake_ && shake_(sum_.generation) == evaluation_context::changed)
653 {
655
656 if (const auto best(sum_.best()); !best.empty())
657 {
658 sum_.set_best_fitness(eva_(best.ind));
659 progress_best = sum_.best();
660 }
661 }
662
664 pop_.layers(), pop_.problem().params.min_samples_per_eval_thread, eva_);
665
666 ultraDEBUG("Launching tasks for generation {}", sum_.generation);
667 const auto subpops(pop_.range_of_layers());
668
669 for (auto l(subpops.begin()); l != subpops.end(); ++l)
670 tasks.submit(evolve_subpop, l);
671
672 ultraDEBUG("Tasks running");
673
674 while (!tasks.wait_for(10ms))
675 {
676 if (ps.from_last_msg.elapsed() > 2s
677 && !print_and_update_if_better(sum_.best()))
678 print(message::status, ps);
679
680 if (!source.stop_requested() && stop_condition()) [[unlikely]]
681 {
682 source.request_stop();
683 ultraDEBUG("Sending closing message to tasks");
684 }
685 }
686
687 print_and_update_if_better(sum_.best());
688
689 if (source.stop_requested())
690 break;
691
692 if constexpr (RefinementStrategy<ES<E>, decltype(pop_)>)
693 {
694 if (refinement_callback_)
695 {
696 if (ref_sched.ready(sum_.stagnation()))
697 {
698 bool performed(false);
699
700 if (auto *ref_pop = strategy.refinement_subgroup(pop_))
701 performed = perform_refinement(*ref_pop, ps);
702
703 ref_sched.after_refinement_attempt(performed);
704
705 // `performed` only means refinement ran. It may still have updated
706 // and reported `sum_`, so refresh the local progress cache.
707 if (performed)
708 progress_best = sum_.best();
709 }
710 else
711 ref_sched.tick();
712 }
713 }
714
715 sum_.az = analyzer(pop_, eva_);
716 if (search_log_)
717 search_log_->save_snapshot(pop_, sum_);
718
719 strategy.after_generation(pop_, sum_); // strategy-specific bookkeeping
720 if (after_generation_callback_)
721 after_generation_callback_(pop_, sum_);
722
723 ++sum_.generation;
724 }
725
726 sum_.elapsed = ps.from_start.elapsed();
727
728 if (emit_messages_)
729 ultraINFO("Evolution completed at generation: {}. Elapsed time: {}",
730 sum_.generation,
731 lexical_cast<std::string>(ps.from_start.elapsed()));
732
733 return sum_;
734}
735
741template<Evaluator E>
742bool evolution<E>::is_valid() const
743{
744 return pop_.is_valid();
745}
746
747#endif // include guard
Definition evolution_summary.h:33
std::size_t set_evaluator_parallelism(std::size_t allocated, std::size_t min_samples_per_eval_thread, E &eva)
Definition evolution.tcc:166
void invalidate_cache_if_supported(E &eva)
Definition evaluation_context.h:48
static constexpr std::size_t max_tournament_size
Definition parameters.h:180
Definition scored_individual.h:48