Skip to content

Commit 9912ee6

Browse files
authored
Merge pull request #65 from heal-research/feat/phase-timer-eve-default
feat: PhaseTimer instrumentation + Eve as default math backend
2 parents 412ad01 + ad15a49 commit 9912ee6

10 files changed

Lines changed: 142 additions & 34 deletions

File tree

CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Operon — Claude guidance
2+
3+
## External libraries
4+
5+
When working with any external library, consult the official docs first before diving into source code.
6+
7+
- Taskflow: https://taskflow.github.io/

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ if (USE_JEMALLOC)
136136
endif()
137137

138138
if(NOT MATH_BACKEND)
139-
set(MATH_BACKEND "Eigen")
139+
set(MATH_BACKEND "Eve")
140140
endif()
141141

142142
message(STATUS "MATH: ${MATH_BACKEND}")

cli/source/reporter.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include <fmt/format.h>
55
#include <operon/algorithms/ga_base.hpp>
6+
#include <operon/algorithms/phase_timer.hpp>
67

78
#include <string>
89
#include <taskflow/taskflow.hpp>
@@ -191,7 +192,7 @@ class Reporter {
191192
T{ "jac_eval", jacEval, ":>" },
192193
T{ "opt_time", cfTime, ":>" },
193194
T{ "seed", config.Seed, ":>10" },
194-
T{ "sort_ms", gp.SortTime() * 1e3, format },
195+
T{ "sort_ms", [&]{ auto const& t = gp.Timings(); auto it = t.find(std::string{SortTaskName}); return it != t.end() ? it->second * 1e3 : 0.0; }(), format },
195196
T{ "elapsed", gp.Elapsed(), ":>"},
196197
};
197198
PrintStats({ stats.begin(), stats.end() }, gp.Generation() == 0);

cmake/variables.cmake

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ if(PROJECT_IS_TOP_LEVEL)
1212
set(JEMALLOC_DESCRIPTION "Link against jemalloc, a general purpose malloc(3) implementation that emphasizes fragmentation avoidance and scalable concurrency support [default=OFF].")
1313
set(USE_SINGLE_PRECISION_DESCRIPTION "Perform model evaluation using floats (single precision) instead of doubles. Great for reducing runtime, might not be appropriate for all purposes [default=OFF].")
1414
set(USE_CERES_DESCRIPTION "Use the non-linear least squares optimizer from Ceres solver to tune model coefficients (if OFF, Eigen::LevenbergMarquardt will be used instead).")
15-
set(MATH_BACKEND_DESCRIPTION "Math library for tree evaluation (defaults to Eigen)")
15+
set(MATH_BACKEND_DESCRIPTION "Math library for tree evaluation (defaults to Eve)")
1616

1717
# option descriptions
1818
option(USE_JEMALLOC ${JEMALLOC_DESCRIPTION} OFF)
1919
option(USE_SINGLE_PRECISION ${USE_SINGLE_PRECISION_DESCRIPTION} ON)
2020
option(USE_CERES ${USE_CERES_DESCRIPTION} OFF)
21-
option(MATH_BACKEND ${MATH_BACKEND_DESCRIPTION} "Eigen")
21+
option(MATH_BACKEND ${MATH_BACKEND_DESCRIPTION} "Eve")
2222

2323
# provide a summary of configured options
2424
include(FeatureSummary)

include/operon/algorithms/ga_base.hpp

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
#define GA_BASE_HPP
66

77
#include <functional>
8+
#include <string>
89
#include <operon/operon_export.hpp>
10+
#include "operon/core/types.hpp"
911
#include "operon/operators/generator.hpp"
1012
#include "config.hpp"
1113

@@ -61,16 +63,20 @@ class GeneticAlgorithmBase {
6163
[[nodiscard]] auto Elapsed() const -> double { return elapsed_; }
6264
auto Elapsed() -> double& { return elapsed_; }
6365

64-
[[nodiscard]] auto SortTime() const -> double { return sort_time_; }
65-
auto SortTime() -> double& { return sort_time_; }
66+
[[nodiscard]] auto Timings() const -> Operon::Map<std::string, double> const& { return phaseTimes_; }
67+
auto Timings() -> Operon::Map<std::string, double>& { return phaseTimes_; }
6668

6769
[[nodiscard]] auto IsFitted() const -> bool { return isFitted_; }
6870
auto IsFitted() -> bool& { return isFitted_; }
6971

72+
// Valid to call between runs only. The PhaseTimer observer owns its own
73+
// totals and is recreated each Run(), so Reset() mid-run would cause the
74+
// next reportProgress sync to overwrite the cleared map with stale data.
7075
auto Reset() -> void
7176
{
7277
generation_ = 0;
7378
elapsed_ = 0;
79+
phaseTimes_.clear();
7480
GetGenerator()->Evaluator()->Reset();
7581
}
7682

@@ -97,8 +103,8 @@ class GeneticAlgorithmBase {
97103
Operon::Span<Individual> offspring_;
98104

99105
size_t generation_{0};
100-
double elapsed_{0}; // elapsed time in seconds
101-
double sort_time_{0}; // cumulative non-dominated sort time in seconds
106+
double elapsed_{0};
107+
Operon::Map<std::string, double> phaseTimes_;
102108
bool isFitted_{false};
103109
};
104110

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// SPDX-License-Identifier: MIT
2+
// SPDX-FileCopyrightText: Copyright 2019-2023 Heal Research
3+
4+
#pragma once
5+
6+
#include <chrono>
7+
#include <mutex>
8+
#include <string>
9+
#include <string_view>
10+
#include <vector>
11+
12+
#include <taskflow/core/observer.hpp>
13+
14+
#include "operon/core/types.hpp"
15+
16+
namespace Operon {
17+
18+
// Task name constants — used in algorithm implementations and reporters to
19+
// avoid magic-string coupling between the task .name() call and any lookup site.
20+
inline constexpr std::string_view SortTaskName = "non-dominated sort";
21+
22+
namespace detail {
23+
// Transparent hash for std::string keys: avoids constructing a std::string
24+
// on every on_exit call after the first encounter of each task name.
25+
struct StringHash {
26+
using is_transparent = void;
27+
using is_avalanching = void;
28+
auto operator()(std::string_view sv) const noexcept -> uint64_t {
29+
return ankerl::unordered_dense::hash<std::string_view>{}(sv);
30+
}
31+
};
32+
} // namespace detail
33+
34+
class PhaseTimer final : public tf::ObserverInterface {
35+
using Clock = std::chrono::steady_clock;
36+
using Totals = Operon::Map<std::string, double, detail::StringHash, std::equal_to<>>;
37+
38+
std::vector<Clock::time_point> entry_; // per-worker; each worker writes only its own slot
39+
mutable std::mutex mtx_;
40+
Totals totals_; // phase name -> cumulative seconds
41+
42+
public:
43+
void set_up(size_t numWorkers) override {
44+
entry_.resize(numWorkers);
45+
}
46+
47+
void on_entry(tf::WorkerView w, tf::TaskView tv) override {
48+
if (tv.name().empty()) { return; }
49+
entry_[w.id()] = Clock::now();
50+
}
51+
52+
void on_exit(tf::WorkerView w, tf::TaskView tv) override {
53+
if (tv.name().empty()) { return; }
54+
auto const dt = std::chrono::duration<double>(Clock::now() - entry_[w.id()]).count();
55+
std::scoped_lock lock{mtx_};
56+
totals_[tv.name()] += dt; // transparent lookup: no std::string construction after first insert
57+
}
58+
59+
// Returns a snapshot of accumulated timings as a plain map.
60+
// Note: a new PhaseTimer is created per Run() call, so totals_ always
61+
// reflects a single run. Reset() on the algorithm clears phaseTimes_ on
62+
// the base but does not affect any in-flight observer.
63+
[[nodiscard]] auto Timings() const -> Operon::Map<std::string, double> {
64+
std::scoped_lock lock{mtx_};
65+
return { totals_.begin(), totals_.end() };
66+
}
67+
};
68+
69+
} // namespace Operon

include/operon/interpreter/backend/eve/functions.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ namespace Operon::Backend {
6565

6666
template<typename T, std::size_t S>
6767
requires (S % eve::wide<T>::size() == 0)
68-
auto Min(T* res, T weight, auto const*... args) {
68+
auto Min(T* res, [[maybe_unused]] T weight, auto const*... args) {
6969
return eve::algo::transform_to(eve::views::zip(std::span{args, S}...), res, eve::min);
7070
}
7171

7272
template<typename T, std::size_t S>
7373
requires (S % eve::wide<T>::size() == 0)
74-
auto Max(T* res, T weight, auto const*... args) {
74+
auto Max(T* res, [[maybe_unused]] T weight, auto const*... args) {
7575
return eve::algo::transform_to(eve::views::zip(std::span{args, S}...), res, eve::max);
7676
}
7777

source/algorithms/gp.cpp

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// NOLINTEND(misc-include-cleaner)
1717

1818
#include "operon/algorithms/gp.hpp"
19+
#include "operon/algorithms/phase_timer.hpp"
1920
#include "operon/core/contracts.hpp" // for ENSURE
2021
#include "operon/core/types.hpp"
2122
#include "operon/operators/initializer.hpp"
@@ -66,10 +67,12 @@ auto GeneticProgrammingAlgorithm::Run(tf::Executor& executor, Operon::RandomGene
6667
auto parents = Parents();
6768
auto offspring = Offspring();
6869

70+
auto timer = executor.make_observer<PhaseTimer>();
71+
6972
// while loop control flow
7073
tf::Taskflow taskflow;
7174
auto [init, cond, body, back, done] = taskflow.emplace(
72-
[&](tf::Subflow& subflow) -> void {
75+
[&, timer](tf::Subflow& subflow) -> void {
7376
auto prepareEval = subflow.emplace([&]() -> void { evaluator->Prepare(parents); }).name("prepare evaluator");
7477
auto eval = subflow.for_each_index(size_t { 0 }, parents.size(), size_t { 1 }, [&](size_t i) -> void {
7578
auto id = executor.this_worker_id();
@@ -80,7 +83,10 @@ auto GeneticProgrammingAlgorithm::Run(tf::Executor& executor, Operon::RandomGene
8083
parents[i].Fitness = (*evaluator)(rngs[i], parents[i], slots[id]);
8184
})
8285
.name("evaluate population");
83-
auto reportProgress = subflow.emplace([&]() -> void { if (report) { std::invoke(report); } }).name("report progress");
86+
auto reportProgress = subflow.emplace([&, timer]() -> void {
87+
Timings() = timer->Timings();
88+
if (report) { std::invoke(report); }
89+
}).name("report progress");
8490
prepareEval.precede(eval);
8591
eval.precede(reportProgress);
8692

@@ -95,7 +101,7 @@ auto GeneticProgrammingAlgorithm::Run(tf::Executor& executor, Operon::RandomGene
95101
}
96102
}, // init
97103
stop, // loop condition
98-
[&](tf::Subflow& subflow) -> void {
104+
[&, timer](tf::Subflow& subflow) -> void {
99105
auto keepElite = subflow.emplace([&]() -> void {
100106
offspring[0] = *std::ranges::min_element(parents, [&](const auto& lhs, const auto& rhs) -> auto { return lhs[idx] < rhs[idx]; });
101107
})
@@ -114,7 +120,10 @@ auto GeneticProgrammingAlgorithm::Run(tf::Executor& executor, Operon::RandomGene
114120
.name("generate offspring");
115121
auto reinsert = subflow.emplace([&]() -> void { (*reinserter)(random, Parents(), offspring); }).name("reinsert");
116122
auto incrementGeneration = subflow.emplace([&]() -> void { ++Generation(); }).name("increment generation");
117-
auto reportProgress = subflow.emplace([&]() -> void { if (report) { std::invoke(report); } }).name("report progress");
123+
auto reportProgress = subflow.emplace([&, timer]() -> void {
124+
Timings() = timer->Timings();
125+
if (report) { std::invoke(report); }
126+
}).name("report progress");
118127

119128
// set-up subflow graph
120129
keepElite.precede(prepareGenerator);
@@ -139,8 +148,9 @@ auto GeneticProgrammingAlgorithm::Run(tf::Executor& executor, Operon::RandomGene
139148
body.precede(back);
140149
back.precede(cond);
141150

142-
executor.run(taskflow);
143-
executor.wait_for_all();
151+
executor.run(taskflow).wait();
152+
Timings() = timer->Timings();
153+
executor.remove_observer(std::move(timer));
144154
}
145155

146156
auto GeneticProgrammingAlgorithm::Run(Operon::RandomGenerator& random, std::function<void()> report, size_t threads, bool warmStart) -> void

source/algorithms/nsga2.cpp

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <vector> // for vector, vector::size_type
1717

1818
#include "operon/algorithms/nsga2.hpp"
19+
#include "operon/algorithms/phase_timer.hpp"
1920
#include "operon/core/contracts.hpp" // for ENSURE
2021
#include "operon/core/operator.hpp" // for OperatorBase
2122
#include "operon/core/problem.hpp" // for Problem
@@ -78,9 +79,7 @@ auto NSGA2::Sort(Operon::Span<Individual> pop) -> void
7879
auto r = std::stable_partition(pop.begin(), pop.end(), [](auto const& ind) -> auto { return !ind.Rank; });
7980
Operon::Span<Operon::Individual const> const uniq(pop.begin(), r);
8081
// do the sorting
81-
auto const ts = std::chrono::steady_clock::now();
8282
fronts_ = (*sorter_)(uniq, eps);
83-
SortTime() += std::chrono::duration<double>(std::chrono::steady_clock::now() - ts).count();
8483
// sort the fronts for consistency between sorting algos
8584
for (auto& f : fronts_) {
8685
std::stable_sort(f.begin(), f.end());
@@ -143,9 +142,11 @@ auto NSGA2::Run(tf::Executor& executor, Operon::RandomGenerator& random, std::fu
143142
auto offspring = Offspring();
144143

145144
// while loop control flow
145+
auto timer = executor.make_observer<PhaseTimer>();
146+
146147
tf::Taskflow taskflow;
147148
auto [init, cond, body, back, done] = taskflow.emplace(
148-
[&](tf::Subflow& subflow) -> void {
149+
[&, timer](tf::Subflow& subflow) -> void {
149150
auto prepareEval = subflow.emplace([&]() -> void { evaluator->Prepare(parents); }).name("prepare evaluator");
150151
auto eval = subflow.for_each_index(size_t { 0 }, parents.size(), size_t { 1 }, [&](size_t i) -> void {
151152
// make sure the worker has a large enough buffer
@@ -154,8 +155,9 @@ auto NSGA2::Run(tf::Executor& executor, Operon::RandomGenerator& random, std::fu
154155
parents[i].Fitness = (*evaluator)(rngs[i], parents[i], slots[id]);
155156
})
156157
.name("evaluate population");
157-
auto nonDominatedSort = subflow.emplace([&]() -> void { Sort(parents); }).name("non-dominated sort");
158-
auto reportProgress = subflow.emplace([&]() -> void {
158+
auto nonDominatedSort = subflow.emplace([&]() -> void { Sort(parents); }).name(std::string{SortTaskName});
159+
auto reportProgress = subflow.emplace([&, timer]() -> void {
160+
Timings() = timer->Timings();
159161
if (report) {
160162
std::invoke(report);
161163
}
@@ -176,7 +178,7 @@ auto NSGA2::Run(tf::Executor& executor, Operon::RandomGenerator& random, std::fu
176178
}
177179
}, // init
178180
stop, // loop condition
179-
[&](tf::Subflow& subflow) -> void {
181+
[&, timer](tf::Subflow& subflow) -> void {
180182
auto prepareGenerator = subflow.emplace([&]() -> void { generator->Prepare(parents); }).name("prepare generator");
181183
auto generateOffspring = subflow.for_each_index(size_t { 0 }, offspring.size(), size_t { 1 }, [&](size_t i) -> void {
182184
slots[executor.this_worker_id()].resize(trainSize);
@@ -191,10 +193,13 @@ auto NSGA2::Run(tf::Executor& executor, Operon::RandomGenerator& random, std::fu
191193
}
192194
})
193195
.name("generate offspring");
194-
auto nonDominatedSort = subflow.emplace([&]() -> void { Sort(individuals); }).name("non-dominated sort");
196+
auto nonDominatedSort = subflow.emplace([&]() -> void { Sort(individuals); }).name(std::string{SortTaskName});
195197
auto reinsert = subflow.emplace([&]() -> void { reinserter->Sort(individuals); }).name("reinsert");
196198
auto incrementGeneration = subflow.emplace([&]() -> void { ++Generation(); }).name("increment generation");
197-
auto reportProgress = subflow.emplace([&]() -> void { if (report) { std::invoke(report); } }).name("report progress");
199+
auto reportProgress = subflow.emplace([&, timer]() -> void {
200+
Timings() = timer->Timings();
201+
if (report) { std::invoke(report); }
202+
}).name("report progress");
198203

199204
// set-up subflow graph
200205
prepareGenerator.precede(generateOffspring);
@@ -219,8 +224,9 @@ auto NSGA2::Run(tf::Executor& executor, Operon::RandomGenerator& random, std::fu
219224
body.precede(back);
220225
back.precede(cond);
221226

222-
executor.run(taskflow);
223-
executor.wait_for_all();
227+
executor.run(taskflow).wait();
228+
Timings() = timer->Timings();
229+
executor.remove_observer(std::move(timer));
224230
}
225231

226232
auto NSGA2::Run(Operon::RandomGenerator& random, std::function<void()> report, size_t threads, bool warmStart) -> void

0 commit comments

Comments
 (0)