Wildmeshing Toolkit
Loading...
Searching...
No Matches
ExecutionScheduler.hpp
1#pragma once
2
3#include <wmtk/TetMesh.h>
4#include <wmtk/TriMesh.h>
5#include <wmtk/threading/concurrent_priority_queue.hpp>
6#include <wmtk/threading/serial_priority_queue.hpp>
7#include <wmtk/threading/task_group.hpp>
8#include <wmtk/utils/Logger.hpp>
9
10// clang-format off
11#include <functional>
12#include <limits>
13#include <wmtk/utils/DisableWarnings.hpp>
14#include <wmtk/utils/EnableWarnings.hpp>
15// clang-format on
16
17#include <algorithm>
18#include <atomic>
19#include <cassert>
20#include <chrono>
21#include <cstddef>
22#include <queue>
23#include <stdexcept>
24#include <thread>
25#include <type_traits>
26
27namespace wmtk {
28enum class ExecutionPolicy { kSeq, kUnSeq, kPartition, kColor, kMax };
29
30using Op = std::string;
31
32template <class AppMesh>
34{
35 using Tuple = typename AppMesh::Tuple;
40 std::map<
41 Op, // strings
42 std::function<std::optional<std::vector<Tuple>>(AppMesh&, const Tuple&)>>
48 std::function<double(const AppMesh&, Op op, const Tuple&)> priority =
49 [](const AppMesh&, Op, const Tuple&) { return 0.; };
54 std::function<bool(double)> should_renew = [](double) { return true; };
59 std::function<std::vector<std::pair<Op, Tuple>>(const AppMesh&, Op, const std::vector<Tuple>&)>
61 [](const AppMesh&, Op, const std::vector<Tuple>&) -> std::vector<std::pair<Op, Tuple>> {
62 return {};
63 };
68 std::function<bool(AppMesh&, const Tuple&, int task_id)> lock_vertices =
69 [](const AppMesh&, const Tuple&, int task_id) { return true; };
77 std::function<bool(const AppMesh&)> stopping_criterion = [](const AppMesh&) {
78 return false; // non-stop, process everything
79 };
99 size_t stopping_criterion_checking_frequency = std::numeric_limits<size_t>::max();
106 std::function<bool(const AppMesh&, const std::tuple<double, Op, Tuple>& t)>
107 is_weight_up_to_date = [](const AppMesh& m, const std::tuple<double, Op, Tuple>& t) {
108 // always do.
109 assert(std::get<2>(t).is_valid(m));
110 return true;
111 };
115 std::function<void(const AppMesh&, Op, const Tuple& t)> on_fail =
116 [](const AppMesh&, Op, const Tuple& t) {};
117
118 ExecutionPolicy policy;
119
120 int num_threads = 1;
121
142 size_t max_retry_limit = 10;
143
166 size_t deferral_window = 128;
173 ExecutePass(const ExecutionPolicy& policy_ = ExecutionPolicy::kSeq)
174 : policy(policy_)
175 {
176 if constexpr (std::is_base_of<TetMesh, AppMesh>::value) {
178 {"edge_collapse",
179 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
180 std::vector<Tuple> ret;
181 if (m.collapse_edge(t, ret))
182 return ret;
183 else
184 return {};
185 }},
186 {"edge_swap",
187 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
188 std::vector<Tuple> ret;
189 if (m.swap_edge(t, ret))
190 return ret;
191 else
192 return {};
193 }},
194 {"edge_swap_44",
195 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
196 std::vector<Tuple> ret;
197 if (m.swap_edge_44(t, ret))
198 return ret;
199 else
200 return {};
201 }},
202 {"edge_swap_56",
203 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
204 std::vector<Tuple> ret;
205 if (m.swap_edge_56(t, ret))
206 return ret;
207 else
208 return {};
209 }},
210 {"edge_split",
211 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
212 std::vector<Tuple> ret;
213 if (m.split_edge(t, ret))
214 return ret;
215 else
216 return {};
217 }},
218 {"face_swap",
219 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
220 std::vector<Tuple> ret;
221 if (m.swap_face(t, ret))
222 return ret;
223 else
224 return {};
225 }},
226 {"vertex_smooth",
227 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
228 if (m.smooth_vertex(t))
229 return std::vector<Tuple>{};
230 else
231 return {};
232 }},
233 {"face_split",
234 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
235 std::vector<Tuple> ret;
236 if (m.split_face(t, ret))
237 return ret;
238 else
239 return {};
240 }},
241 {"tet_split", [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
242 std::vector<Tuple> ret;
243 if (m.split_tet(t, ret))
244 return ret;
245 else
246 return {};
247 }}};
248 }
249 if constexpr (std::is_base_of<TriMesh, AppMesh>::value) {
251 {"edge_collapse",
252 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
253 std::vector<Tuple> ret;
254 if (m.collapse_edge(t, ret))
255 return ret;
256 else
257 return {};
258 }},
259 {"edge_swap",
260 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
261 std::vector<Tuple> ret;
262 if (m.swap_edge(t, ret))
263 return ret;
264 else
265 return {};
266 }},
267 {"edge_split",
268 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
269 std::vector<Tuple> ret;
270 if (m.split_edge(t, ret))
271 return ret;
272 else
273 return {};
274 }},
275 {"vertex_smooth",
276 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
277 if (m.smooth_vertex(t))
278 return std::vector<Tuple>{};
279 else
280 return {};
281 }},
282 {"face_split", [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
283 std::vector<Tuple> ret;
284 if (m.split_face(t, ret))
285 return ret;
286 else
287 return {};
288 }}};
289 }
290 };
291
292 ExecutePass(ExecutePass&) = delete;
293
294private:
295 void operation_cleanup(AppMesh& m)
296 { //
297 // class ResourceManger
298 // what about RAII mesh edit locking?
299 // release mutex, but this should be implemented in TetMesh class.
300 if (policy == ExecutionPolicy::kSeq)
301 return;
302 else {
303 m.release_vertex_mutex_in_stack();
304 }
305 }
306
307 size_t get_partition_id(const AppMesh& m, const Tuple& e)
308 {
309 if (policy == ExecutionPolicy::kSeq) {
310 return 0;
311 }
312 return m.get_partition_id(e);
313 }
314
315public:
324 bool operator()(AppMesh& m, const std::vector<std::pair<Op, Tuple>>& operation_tuples)
325 {
326 // The queue holds an operation's INDEX rather than its name. edit_operation_maps is a
327 // std::map, so iterating it yields the names in lexicographic order and an index is
328 // exactly a name's rank among them -- comparing indices is therefore identical to
329 // comparing the strings, which is what keeps the queue order, and so the output,
330 // bit-for-bit unchanged. What it buys is that a queue element is now trivially
331 // copyable: a heap sift moves 8 bytes instead of a std::string, and a tie on the
332 // priority is an integer compare instead of a string compare. It also turns the
333 // per-operation dispatch from a string-keyed std::map lookup into an index.
334 using OpId = uint32_t;
335 using Elem = std::tuple<double, OpId, Tuple, size_t>; // priority, op index, tuple, #retries
336 // Each task owns its queue outright -- it is seeded before any thread starts, and the
337 // task both pops from it and pushes its renewed operations back into it -- so those need
338 // no lock. `final_queue` is the one that genuinely crosses threads: tasks push retry
339 // overflow into it while running, and it is drained after the barrier. Both are
340 // std::priority_queue with the same comparator underneath, so pop order is unchanged.
343
344 std::vector<const Op*> op_name;
345 std::vector<std::function<std::optional<std::vector<Tuple>>(AppMesh&, const Tuple&)>*>
346 op_fn;
347 std::map<Op, OpId> op_id;
348 op_name.reserve(edit_operation_maps.size());
349 op_fn.reserve(edit_operation_maps.size());
350 for (auto& kv : edit_operation_maps) {
351 op_id.emplace(kv.first, OpId(op_name.size()));
352 op_name.push_back(&kv.first);
353 op_fn.push_back(&kv.second);
354 }
355 // An operation with no entry here was previously default-constructed into the map by
356 // operator[] and then called, which throws bad_function_call -- so it cannot occur in
357 // any working configuration. Say so plainly rather than ordering it arbitrarily.
358 const auto id_of = [&op_id](const Op& name) {
359 const auto it = op_id.find(name);
360 if (it == op_id.end()) {
361 log_and_throw_error("No operation registered under the name '{}'.", name);
362 }
363 return it->second;
364 };
365
366 std::atomic<bool> stop(false);
367 cnt_success = 0;
368 cnt_fail = 0;
369
370 // Whether anything actually watches the success count *while the pass runs*. When no
371 // stopping criterion is configured -- the case for every tetwild/triwild/simwild pass --
372 // nobody does, and the counters can be accumulated per task and folded in at the end
373 // instead of hammering one shared cache line from every thread on every operation.
374 const bool track_live_success =
375 stopping_criterion_checking_frequency != std::numeric_limits<size_t>::max();
376 std::atomic<size_t> live_success(0);
377
378 std::vector<LocalQueue> queues(num_threads);
379 SharedQueue final_queue;
380
381 // Contention accounting. Everything here is either a per-task local folded in once or a
382 // write to the task's own slot, so it adds nothing to the inner loop. It answers the
383 // two questions the pass could not previously be asked: how often ring acquisition
384 // loses a race, and how much of a "parallel" pass is really the serial drain.
385 m_stats = PassStats{};
386 std::atomic<size_t> lock_failures(0);
387 std::atomic<size_t> overflowed(0);
388 std::vector<double> task_seconds(queues.size(), 0.);
389
390 auto run_single_queue = [&](auto& Q, int task_id) {
391 // Per-task tallies folded into the shared counters once, on the way out. The guard
392 // is RAII rather than a line at the bottom because the loop below has early
393 // returns.
394 struct CountFlusher
395 {
396 std::atomic_int& success_total;
397 std::atomic_int& fail_total;
398 std::atomic<size_t>& lock_failure_total;
399 std::atomic<size_t>& overflow_total;
400 int success = 0;
401 int fail = 0;
402 size_t lock_failure = 0;
403 size_t overflow = 0;
404 ~CountFlusher()
405 {
406 success_total.fetch_add(success, std::memory_order_relaxed);
407 fail_total.fetch_add(fail, std::memory_order_relaxed);
408 lock_failure_total.fetch_add(lock_failure, std::memory_order_relaxed);
409 overflow_total.fetch_add(overflow, std::memory_order_relaxed);
410 }
411 } counts{cnt_success, cnt_fail, lock_failures, overflowed};
412
413 Elem ele_in_queue;
414 // Operations that lost a race for their ring wait here rather than going straight
415 // back into the live queue. Pushing them back immediately is a spin: the element was
416 // just popped as the queue's maximum, `retry` is the last tie-break key in Elem, so
417 // the requeued copy compares strictly greater than what was popped and nothing else
418 // was added -- it comes right back off the top and is retried against a conflict that
419 // has had no time to clear. Deferring lets every other operation this task owns run
420 // first, which is both useful work and the delay the conflict needs.
421 std::vector<Elem> second_chance;
422 // Operations popped since the deferred list was last given back to the queue.
423 // Waiting for the queue to drain completely can mean thousands of operations, by
424 // which time the mesh around a deferred operation has moved and its work has to be
425 // rediscovered. A bounded window gives the conflict time to clear without letting
426 // the operation go stale. 0 = only when the queue empties.
427 size_t since_refill = 0;
428 const auto refill = [&] {
429 for (auto& e : second_chance) {
430 Q.emplace(std::move(e));
431 }
432 second_chance.clear();
433 since_refill = 0;
434 };
435 for (;;) {
436 if (!second_chance.empty() && deferral_window > 0 &&
437 since_refill >= deferral_window) {
438 refill();
439 }
440 if (!Q.try_pop(ele_in_queue)) {
441 if (second_chance.empty()) {
442 break;
443 }
444 // Queue exhausted: the deferred operations have now had everything else
445 // run ahead of them, so give them another go. `retry` still increments on
446 // each attempt and still overflows to final_queue at max_retry_limit, so
447 // this terminates after at most that many rounds.
448 refill();
449 std::this_thread::yield();
450 continue;
451 }
452 ++since_refill;
453 auto& [weight, op, tup, retry] = ele_in_queue;
454 if (!tup.is_valid(m)) {
455 continue;
456 }
457
458 std::vector<Elem> renewed_elements;
459 {
460 auto locked_vid = lock_vertices(
461 m,
462 tup,
463 task_id); // Note that returning `Tuples` would be invalid.
464 if (!locked_vid) {
465 counts.lock_failure++;
466 retry++;
467 if (retry < max_retry_limit) {
468 second_chance.push_back(ele_in_queue);
469 } else {
470 retry = 0;
471 counts.overflow++;
472 final_queue.emplace(ele_in_queue);
473 }
474 continue;
475 }
476 if (tup.is_valid(m)) {
477 const Op& op_str = *op_name[op];
479 m,
480 std::tuple<double, Op, Tuple>(weight, op_str, tup))) {
481 operation_cleanup(m);
482 continue;
483 } // this can encode, in qslim, recompute(energy) == weight.
484 auto newtup = (*op_fn[op])(m, tup);
485 std::vector<std::pair<Op, Tuple>> renewed_tuples;
486 if (newtup) {
487 renewed_tuples = renew_neighbor_tuples(m, op_str, newtup.value());
488 counts.success++;
489 if (track_live_success) {
490 live_success.fetch_add(1, std::memory_order_relaxed);
491 }
492 } else {
493 on_fail(m, op_str, tup);
494 counts.fail++;
495 }
496 for (const auto& [o, e] : renewed_tuples) {
497 auto val = priority(m, o, e);
498 if (should_renew(val)) {
499 renewed_elements.emplace_back(val, id_of(o), e, 0);
500 }
501 }
502 }
503 operation_cleanup(m); // Maybe use RAII
504 }
505 for (auto& e : renewed_elements) {
506 Q.emplace(e);
507 }
508
509 if (stop.load(std::memory_order_acquire)) {
510 return;
511 }
512 if (track_live_success && live_success.load(std::memory_order_relaxed) >
514 if (stopping_criterion(m)) {
515 stop.store(true);
516 return;
517 }
518 }
519 }
520 };
521
522 if (policy == ExecutionPolicy::kSeq) {
523 for (const auto& [op, e] : operation_tuples) {
524 if (!e.is_valid(m)) {
525 continue;
526 }
527 final_queue.emplace(priority(m, op, e), id_of(op), e, 0);
528 }
529 run_single_queue(final_queue, 0);
530 } else {
531 for (const auto& [op, e] : operation_tuples) {
532 if (!e.is_valid(m)) {
533 continue;
534 }
535 queues[get_partition_id(m, e)].emplace(priority(m, op, e), id_of(op), e, 0);
536 }
537 // Comment out parallel: work on serial first.
538 using clock = std::chrono::steady_clock;
539 const auto t_parallel = clock::now();
541 for (int task_id = 0; task_id < queues.size(); task_id++) {
542 tg.run([&run_single_queue, &queues, &task_seconds, task_id] {
543 const auto t0 = clock::now();
544 run_single_queue(queues[task_id], task_id);
545 // Each task writes only its own slot.
546 task_seconds[task_id] =
547 std::chrono::duration<double>(clock::now() - t0).count();
548 });
549 }
550 tg.wait();
551 m_stats.parallel_seconds =
552 std::chrono::duration<double>(clock::now() - t_parallel).count();
553 m_stats.final_queue_size = final_queue.size();
554
555 logger().debug("Parallel Complete, remains element {}", final_queue.size());
556
557 const auto t_tail = clock::now();
558 run_single_queue(final_queue, 0);
559 m_stats.serial_tail_seconds =
560 std::chrono::duration<double>(clock::now() - t_tail).count();
561 }
562
563 m_stats.lock_failures = lock_failures.load(std::memory_order_relaxed);
564 m_stats.overflowed = overflowed.load(std::memory_order_relaxed);
565 if (!task_seconds.empty()) {
566 const auto mm = std::minmax_element(task_seconds.begin(), task_seconds.end());
567 m_stats.idlest_task_seconds = *mm.first;
568 m_stats.busiest_task_seconds = *mm.second;
569 }
570
571 logger().info(
572 "executed: {} | success / fail: {} / {}",
573 (int)cnt_success + (int)cnt_fail,
574 (int)cnt_success,
575 (int)cnt_fail);
577 return true;
578 }
579
580 int get_cnt_success() const { return cnt_success; }
581 int get_cnt_fail() const { return cnt_fail; }
582
590 {
593 size_t lock_failures = 0;
595 size_t overflowed = 0;
600 double parallel_seconds = 0.;
601 double serial_tail_seconds = 0.;
605 double idlest_task_seconds = 0.;
606 };
607 const PassStats& stats() const { return m_stats; }
608
609private:
612 void log_contention() const
613 {
614 if (policy == ExecutionPolicy::kSeq || !logger().should_log(spdlog::level::debug)) {
615 return;
616 }
617 const int executed = (int)cnt_success + (int)cnt_fail;
618 const double total = m_stats.parallel_seconds + m_stats.serial_tail_seconds;
619 logger().debug(
620 " contention: {} ring-acquisition failures over {} executed ops ({:.2f} per op); "
621 "{} overflowed to the serial queue ({} queued at the barrier)",
622 m_stats.lock_failures,
623 executed,
624 executed > 0 ? double(m_stats.lock_failures) / executed : 0.,
625 m_stats.overflowed,
626 m_stats.final_queue_size);
627 logger().debug(
628 " time: {:.4}s parallel + {:.4}s serial tail ({:.1f}% of the pass); busiest task "
629 "{:.4}s, idlest {:.4}s",
630 m_stats.parallel_seconds,
631 m_stats.serial_tail_seconds,
632 total > 0. ? 100. * m_stats.serial_tail_seconds / total : 0.,
633 m_stats.busiest_task_seconds,
634 m_stats.idlest_task_seconds);
635 }
636
637 // Totals for the whole pass. Written once per task, at the end -- see CountFlusher.
638 std::atomic_int cnt_success = 0;
639 std::atomic_int cnt_fail = 0;
640 PassStats m_stats;
641};
642} // namespace wmtk
Definition concurrent_priority_queue.hpp:14
Definition serial_priority_queue.hpp:23
Definition task_group.hpp:14
What the last pass cost in contention, as opposed to in work.
Definition ExecutionScheduler.hpp:590
size_t final_queue_size
Size of that queue once every task had finished.
Definition ExecutionScheduler.hpp:597
size_t lock_failures
Definition ExecutionScheduler.hpp:593
double parallel_seconds
Definition ExecutionScheduler.hpp:600
size_t overflowed
Operations that exhausted max_retry_limit and were pushed to the post-barrier queue.
Definition ExecutionScheduler.hpp:595
double busiest_task_seconds
Definition ExecutionScheduler.hpp:604
Definition ExecutionScheduler.hpp:34
size_t max_retry_limit
Attempts an operation gets at claiming its ring before it is handed to the serial queue drained after...
Definition ExecutionScheduler.hpp:142
std::function< void(const AppMesh &, Op, const Tuple &t)> on_fail
used to collect operations that are not finished and used for later re-execution
Definition ExecutionScheduler.hpp:115
void log_contention() const
Definition ExecutionScheduler.hpp:612
std::function< bool(const AppMesh &, const std::tuple< double, Op, Tuple > &t)> is_weight_up_to_date
Should Process drops some Tuple from being processed. For example, if the energy is out-dated....
Definition ExecutionScheduler.hpp:107
std::function< bool(AppMesh &, const Tuple &, int task_id)> lock_vertices
lock the vertices concerned depends on the operation
Definition ExecutionScheduler.hpp:68
std::function< std::vector< std::pair< Op, Tuple > >(const AppMesh &, Op, const std::vector< Tuple > &)> renew_neighbor_tuples
renew neighboring Tuples after each operation depends on the operation
Definition ExecutionScheduler.hpp:60
size_t stopping_criterion_checking_frequency
Cumulative successful operations before stopping_criterion is first consulted.
Definition ExecutionScheduler.hpp:99
std::function< double(const AppMesh &, Op op, const Tuple &)> priority
Priority function (default to edge length)
Definition ExecutionScheduler.hpp:48
ExecutePass(const ExecutionPolicy &policy_=ExecutionPolicy::kSeq)
Construct a new Execute Pass object. It contains the name-to-operation map and the functions that def...
Definition ExecutionScheduler.hpp:173
bool operator()(AppMesh &m, const std::vector< std::pair< Op, Tuple > > &operation_tuples)
Executes the operations for an application when the lambda function is invoked. The rules that are cu...
Definition ExecutionScheduler.hpp:324
size_t deferral_window
Operations to run before handing deferred operations back to the queue, or 0 to wait until the queue ...
Definition ExecutionScheduler.hpp:166
std::function< bool(double)> should_renew
check on wheather new operations should be added to the priority queue
Definition ExecutionScheduler.hpp:54
std::map< Op, std::function< std::optional< std::vector< Tuple > >(AppMesh &, const Tuple &)> > edit_operation_maps
A dictionary that registers names with operations.
Definition ExecutionScheduler.hpp:43
std::function< bool(const AppMesh &)> stopping_criterion
Stopping Criterion based on the whole mesh For efficiency, not every time is checked....
Definition ExecutionScheduler.hpp:77