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/dynamic_parallel_for.hpp>
7#include <wmtk/threading/serial_priority_queue.hpp>
8#include <wmtk/threading/spin_mutex.hpp>
9#include <wmtk/threading/task_group.hpp>
10#include <wmtk/utils/Logger.hpp>
11
12// clang-format off
13#include <array>
14#include <deque>
15#include <iterator>
16#include <functional>
17#include <limits>
18#include <wmtk/utils/DisableWarnings.hpp>
19#include <wmtk/utils/EnableWarnings.hpp>
20// clang-format on
21
22#include <algorithm>
23#include <atomic>
24#include <cassert>
25#include <chrono>
26#include <cstddef>
27#include <cstdint>
28#include <map>
29#include <memory>
30#include <mutex>
31#include <optional>
32#include <queue>
33#include <stdexcept>
34#include <string>
35#include <thread>
36#include <tuple>
37#include <type_traits>
38#include <unordered_map>
39
40namespace wmtk {
41enum class ExecutionPolicy { kSeq, kUnSeq, kPartition, kColor, kMax };
42
43using Op = std::string;
44
59template <class Tuple>
60class OpList
61{
62public:
63 void emplace_back(const Op& op, const Tuple& t) { m_items.emplace_back(intern(op), t); }
64 size_t size() const { return m_items.size(); }
65 bool empty() const { return m_items.empty(); }
66 void clear() { m_items.clear(); }
67
69 template <class F>
70 void consume(F&& f)
71 {
72 while (!m_items.empty()) {
73 const auto& [id, t] = m_items.front();
74 f(m_names[id], t);
75 m_items.pop_front();
76 }
77 m_items.shrink_to_fit();
78 }
79
81 void append(OpList&& other)
82 {
83 if (m_items.empty() && m_names.empty()) {
84 *this = std::move(other);
85 return;
86 }
87 std::vector<uint32_t> remap(other.m_names.size());
88 for (size_t i = 0; i < remap.size(); ++i) remap[i] = intern(other.m_names[i]);
89 while (!other.m_items.empty()) {
90 m_items.emplace_back(remap[other.m_items.front().first], other.m_items.front().second);
91 other.m_items.pop_front();
92 }
93 }
94
95private:
96 uint32_t intern(const Op& op)
97 {
98 // A list sees one to three distinct names; a scan is cheaper than any map.
99 for (size_t i = 0; i < m_names.size(); ++i) {
100 if (m_names[i] == op) return static_cast<uint32_t>(i);
101 }
102 m_names.push_back(op);
103 return static_cast<uint32_t>(m_names.size() - 1);
104 }
105
106 std::vector<Op> m_names;
107 std::deque<std::pair<uint32_t, Tuple>> m_items;
108};
109
112inline uint64_t edge_queue_key(size_t a, size_t b)
113{
114 if (a > b) std::swap(a, b);
115 if (b >> 32) {
116 log_and_throw_error("edge_queue_key: vertex id {} does not fit in 32 bits", b);
117 }
118 return (uint64_t(a) << 32) | uint64_t(b);
119}
120
121template <class AppMesh>
123{
124 using Tuple = typename AppMesh::Tuple;
129 std::map<
130 Op, // strings
131 std::function<std::optional<std::vector<Tuple>>(AppMesh&, const Tuple&)>>
137 std::function<double(const AppMesh&, Op op, const Tuple&)> priority =
138 [](const AppMesh&, Op, const Tuple&) { return 0.; };
143 std::function<bool(double)> should_renew = [](double) { return true; };
148 std::function<std::vector<std::pair<Op, Tuple>>(const AppMesh&, Op, const std::vector<Tuple>&)>
150 [](const AppMesh&, Op, const std::vector<Tuple>&) -> std::vector<std::pair<Op, Tuple>> {
151 return {};
152 };
157 std::function<bool(AppMesh&, const Tuple&, int task_id)> lock_vertices =
158 [](const AppMesh&, const Tuple&, int task_id) { return true; };
166 std::function<bool(const AppMesh&)> stopping_criterion = [](const AppMesh&) {
167 return false; // non-stop, process everything
168 };
188 size_t stopping_criterion_checking_frequency = std::numeric_limits<size_t>::max();
195 std::function<bool(const AppMesh&, const std::tuple<double, Op, Tuple>& t)>
196 is_weight_up_to_date = [](const AppMesh& m, const std::tuple<double, Op, Tuple>& t) {
197 // always do.
198 assert(std::get<2>(t).is_valid(m));
199 return true;
200 };
204 std::function<void(const AppMesh&, Op, const Tuple& t)> on_fail =
205 [](const AppMesh&, Op, const Tuple& t) {};
206
233 std::function<uint64_t(const AppMesh&, const Op&, const Tuple&)> queue_key;
234 std::function<bool(const AppMesh&, const Op&, const Tuple&)> must_wait;
235 bool is_queued(uint64_t key) const { return m_queued.contains(key); }
238 size_t waits() const { return m_waits.load(); }
239 size_t wait_defects() const { return m_wait_defects.load(); }
240
241 ExecutionPolicy policy;
242
243 int num_threads = 1;
244
265 size_t max_retry_limit = 10;
266
289 size_t deferral_window = 128;
290
326 ExecutePass(const ExecutionPolicy& policy_ = ExecutionPolicy::kSeq)
327 : policy(policy_)
328 {
329 if constexpr (std::is_base_of<TetMesh, AppMesh>::value) {
331 {"edge_collapse",
332 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
333 std::vector<Tuple> ret;
334 if (m.collapse_edge(t, ret))
335 return ret;
336 else
337 return {};
338 }},
339 {"edge_swap",
340 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
341 std::vector<Tuple> ret;
342 if (m.swap_edge(t, ret))
343 return ret;
344 else
345 return {};
346 }},
347 {"edge_swap_44",
348 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
349 std::vector<Tuple> ret;
350 if (m.swap_edge_44(t, ret))
351 return ret;
352 else
353 return {};
354 }},
355 {"edge_swap_56",
356 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
357 std::vector<Tuple> ret;
358 if (m.swap_edge_56(t, ret))
359 return ret;
360 else
361 return {};
362 }},
363 {"edge_split",
364 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
365 std::vector<Tuple> ret;
366 if (m.split_edge(t, ret))
367 return ret;
368 else
369 return {};
370 }},
371 {"face_swap",
372 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
373 std::vector<Tuple> ret;
374 if (m.swap_face(t, ret))
375 return ret;
376 else
377 return {};
378 }},
379 {"vertex_smooth",
380 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
381 if (m.smooth_vertex(t))
382 return std::vector<Tuple>{};
383 else
384 return {};
385 }},
386 {"face_split",
387 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
388 std::vector<Tuple> ret;
389 if (m.split_face(t, ret))
390 return ret;
391 else
392 return {};
393 }},
394 {"tet_split", [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
395 std::vector<Tuple> ret;
396 if (m.split_tet(t, ret))
397 return ret;
398 else
399 return {};
400 }}};
401 }
402 if constexpr (std::is_base_of<TriMesh, AppMesh>::value) {
404 {"edge_collapse",
405 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
406 std::vector<Tuple> ret;
407 if (m.collapse_edge(t, ret))
408 return ret;
409 else
410 return {};
411 }},
412 {"edge_swap",
413 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
414 std::vector<Tuple> ret;
415 if (m.swap_edge(t, ret))
416 return ret;
417 else
418 return {};
419 }},
420 {"edge_split",
421 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
422 std::vector<Tuple> ret;
423 if (m.split_edge(t, ret))
424 return ret;
425 else
426 return {};
427 }},
428 {"vertex_smooth",
429 [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
430 if (m.smooth_vertex(t))
431 return std::vector<Tuple>{};
432 else
433 return {};
434 }},
435 {"face_split", [](AppMesh& m, const Tuple& t) -> std::optional<std::vector<Tuple>> {
436 std::vector<Tuple> ret;
437 if (m.split_face(t, ret))
438 return ret;
439 else
440 return {};
441 }}};
442 }
443 };
444
445 ExecutePass(ExecutePass&) = delete;
446
447private:
448 void operation_cleanup(AppMesh& m)
449 { //
450 // class ResourceManger
451 // what about RAII mesh edit locking?
452 // release mutex, but this should be implemented in TetMesh class.
453 if (policy == ExecutionPolicy::kSeq)
454 return;
455 else {
456 m.release_vertex_mutex_in_stack();
457 }
458 }
459
460 size_t get_partition_id(const AppMesh& m, const Tuple& e)
461 {
462 if (policy == ExecutionPolicy::kSeq) {
463 return 0;
464 }
465 return m.get_partition_id(e);
466 }
467
468public:
477 bool operator()(AppMesh& m, const std::vector<std::pair<Op, Tuple>>& operation_tuples)
478 {
479 return execute(m, [&](const Push& push) {
480 for (const auto& [op, e] : operation_tuples) push(op, e, 0);
481 });
482 }
483
493 bool operator()(AppMesh& m, std::vector<std::pair<Op, Tuple>>&& operation_tuples)
494 {
495 return execute(m, [&](const Push& push) {
496 for (const auto& [op, e] : operation_tuples) push(op, e, 0);
497 // Swapping with an empty vector, not clear(), so the storage is returned.
498 std::vector<std::pair<Op, Tuple>>().swap(operation_tuples);
499 });
500 }
501
506 bool operator()(AppMesh& m, OpList<Tuple>&& operation_tuples)
507 {
508 return execute(m, [&](const Push& push) {
509 operation_tuples.consume([&](const Op& op, const Tuple& t) { push(op, t, 0); });
510 });
511 }
512
513private:
521 using Push = std::function<void(const Op&, const Tuple&, uint64_t)>;
522 using OpFn = std::function<std::optional<std::vector<Tuple>>(AppMesh&, const Tuple&)>;
523
529 struct OpTable
530 {
531 std::vector<const Op*> name;
532 std::vector<OpFn*> fn;
533
534 explicit OpTable(std::map<Op, OpFn>& ops)
535 {
536 name.reserve(ops.size());
537 fn.reserve(ops.size());
538 for (auto& kv : ops) {
539 name.push_back(&kv.first);
540 fn.push_back(&kv.second);
541 }
542 }
543
544 // An operation with no entry here was previously default-constructed into the map by
545 // operator[] and then called, which throws bad_function_call -- so it cannot occur in
546 // any working configuration. Say so plainly rather than ordering it arbitrarily.
547 uint32_t rank(const Op& op) const
548 {
549 const auto it =
550 std::lower_bound(name.begin(), name.end(), op, [](const Op* a, const Op& b) {
551 return *a < b;
552 });
553 if (it == name.end() || **it != op) {
554 log_and_throw_error("No operation registered under the name '{}'.", op);
555 }
556 return uint32_t(it - name.begin());
557 }
558 };
559
568 struct WaitSink
569 {
570 std::mutex mutex;
571 std::deque<std::pair<uint32_t, Tuple>> ops;
572 std::deque<uint64_t> keys;
573 };
574
576 bool run_screened(AppMesh& m, const std::function<void(const Push&)>& seed)
577 {
578 m_stats = PassStats{};
580 const OpTable ops(edit_operation_maps);
581 const bool keyed = bool(queue_key);
582
583 // The candidates, compact (rank, tuple), in a deque so that growing it never holds two
584 // buffers at once. The input list is released as it is read, so it and this never both
585 // hold every candidate.
586 using Cand = std::pair<uint32_t, Tuple>;
587 std::deque<Cand> candidates;
588 seed([&](const Op& op, const Tuple& t, uint64_t) {
589 candidates.emplace_back(ops.rank(op), t);
590 });
591
592 // queue_key bookkeeping (see must_wait) for the whole call, across its rounds: an
593 // operation counts as queued from the moment it is a candidate -- or renewed -- until it
594 // is refused, tried, or dropped. A waiting operation stays queued into the next round.
595 // `keys[i]` is candidate i's key; `keys` stays empty when the pass sets no queue_key.
596 m_queued.clear();
597 const auto done = [&](const uint64_t key) {
598 if (key != 0 && !m_queued.remove(key)) ++m_untracked_done;
599 };
600 std::deque<uint64_t> keys;
601 const auto key_at = [&keys, keyed](size_t i) -> uint64_t { return keyed ? keys[i] : 0; };
602 if (keyed) {
603 keys.resize(candidates.size(), 0);
604 threading::dynamic_parallel_for(
605 candidates.size(),
606 std::max(1, num_threads),
607 256,
608 [&](size_t b, size_t e) {
609 for (size_t i = b; i < e; ++i) {
610 const auto& [op, t] = candidates[i];
611 if (!t.is_valid(m)) continue;
612 keys[i] = queue_key(m, *ops.name[op], t);
613 if (keys[i] != 0) m_queued.add(keys[i]);
614 }
615 });
616 }
617
618 // Operations renewed by a success come back here, to be screened in the next round,
619 // instead of going straight into the commit queues; counted as queued right away, with
620 // the ring still locked. Restored on every way out.
621 using RenewFn = decltype(renew_neighbor_tuples);
622 struct RestoreRenew
623 {
624 RenewFn& slot;
625 RenewFn saved;
626 ~RestoreRenew() { slot = std::move(saved); }
628 WaitSink next; // renewed and waiting operations, with their keys
630 [&next, &ops, keyed, this, driver = restore.saved](
631 const AppMesh& mm,
632 Op op,
633 const std::vector<Tuple>& newts) -> std::vector<std::pair<Op, Tuple>> {
634 const std::vector<std::pair<Op, Tuple>> tups = driver(mm, op, newts);
635 std::vector<Cand> ranked;
636 ranked.reserve(tups.size());
637 for (const auto& [o, t] : tups) ranked.emplace_back(ops.rank(o), t);
638 std::vector<uint64_t> ks;
639 if (keyed) {
640 ks.assign(tups.size(), 0);
641 for (size_t k = 0; k < tups.size(); ++k) {
642 if (!tups[k].second.is_valid(mm)) continue;
643 ks[k] = queue_key(mm, tups[k].first, tups[k].second);
644 if (ks[k] != 0) m_queued.add(ks[k]);
645 }
646 }
647 std::lock_guard<std::mutex> lock(next.mutex);
648 next.ops.insert(next.ops.end(), ranked.begin(), ranked.end());
649 next.keys.insert(next.keys.end(), ks.begin(), ks.end());
650 return {};
651 };
652
653 enum : uint8_t { kStale = 0, kSurvives = 1, kRefused = 2 };
654 constexpr size_t kInlineCommit = 64;
655 size_t attempts = 0, successes = 0, failures = 0, rounds = 0, committed_attempts = 0;
656 const size_t waits_before = m_waits.load();
657 using Clock = std::chrono::steady_clock;
658 Clock::duration screen_time{}, commit_time{};
659 while (!candidates.empty()) {
660 ++rounds;
661 const auto screen_start = Clock::now();
662 // 1. Screen: dry runs, in parallel, against a mesh nobody is changing.
663 std::vector<uint8_t> verdict(candidates.size(), kStale);
664 threading::dynamic_parallel_for(
665 candidates.size(),
666 std::max(1, num_threads),
667 64,
668 [&](size_t b, size_t e) {
669 OperationDryRunScope dry;
670 for (size_t i = b; i < e; ++i) {
671 const auto& [op, t] = candidates[i];
672 if (!t.is_valid(m)) continue;
673 const Op& name = *ops.name[op];
674 if (!is_weight_up_to_date(
675 m,
676 std::tuple<double, Op, Tuple>(priority(m, name, t), name, t))) {
677 continue;
678 }
679 verdict[i] = (*ops.fn[op])(m, t) ? kSurvives : kRefused;
680 }
681 });
682
683 // 2. The refused are failures, reported as the queues would report them; they and
684 // the stale ones are no longer queued. The survivors are compacted to the front, in
685 // place: a second list beside this one would double the round's footprint.
686 size_t n = 0;
687 for (size_t i = 0; i < candidates.size(); ++i) {
688 if (verdict[i] == kSurvives) {
689 if (n != i) {
690 candidates[n] = std::move(candidates[i]);
691 if (keyed) keys[n] = keys[i];
692 }
693 ++n;
694 continue;
695 }
696 done(key_at(i));
697 if (verdict[i] == kRefused) {
698 on_fail(m, *ops.name[candidates[i].first], candidates[i].second);
699 ++failures;
700 ++attempts;
701 }
702 }
703 std::vector<uint8_t>().swap(verdict);
704 candidates.resize(n);
705 candidates.shrink_to_fit();
706 if (keyed) {
707 keys.resize(n);
708 keys.shrink_to_fit();
709 }
710
711 // 3. Commit the survivors for real, through the usual queues and ring locks.
712 const auto commit_start = Clock::now();
713 screen_time += commit_start - screen_start;
714 const size_t committed_before = committed_attempts;
715 if (n > kInlineCommit) {
716 execute(
717 m,
718 [&](const Push& push) {
719 for (size_t i = 0; i < candidates.size(); ++i) {
720 push(*ops.name[candidates[i].first], candidates[i].second, key_at(i));
721 }
722 // Released once queued, so the list and the queues never both hold
723 // every survivor (see operator()).
724 std::deque<Cand>().swap(candidates);
725 std::deque<uint64_t>().swap(keys);
726 },
727 &next);
728 successes += size_t(cnt_success);
729 failures += size_t(cnt_fail);
730 attempts += size_t(cnt_success) + size_t(cnt_fail);
731 committed_attempts += size_t(cnt_success) + size_t(cnt_fail);
732 } else if (n > 0) {
733 const auto inline_start = Clock::now();
734 // Too few to be worth a parallel pass: commit them here, best first, as a
735 // single queue would -- no tasks, no locks, nothing else is running. A chain of
736 // dependent operations, each renewing the next in one spot, comes through as one
737 // survivor per round, thousands of rounds in a row (a 2D split pass: 4973), and
738 // starting the workers for each of those was most of what the pass cost.
739 // Same order as the queue would pop them: its element and comparator.
740 using Elem = std::tuple<double, uint32_t, Tuple, size_t>;
741 std::priority_queue<Elem> q;
742 for (size_t k = 0; k < n; ++k) {
743 const auto& [op, t] = candidates[k];
744 q.emplace(priority(m, *ops.name[op], t), op, t, k);
745 }
746 for (; !q.empty(); q.pop()) {
747 const auto& [weight, op, t, k] = q.top();
748 const Op& name = *ops.name[op];
749 const uint64_t key = key_at(k);
750 if (!t.is_valid(m) ||
751 !is_weight_up_to_date(m, std::tuple<double, Op, Tuple>(weight, name, t))) {
752 done(key);
753 continue;
754 }
755 if (must_wait && must_wait(m, name, t)) {
756 m_waits.fetch_add(1, std::memory_order_relaxed);
757 next.ops.emplace_back(op, t); // still queued
758 if (keyed) next.keys.push_back(key);
759 continue;
760 }
761 // Serial here, as in the queues' serial drain: the storage may grow first.
762 m.reserve_free_slots(m.cell_slot_bound(t), 1);
763 auto newtup = (*ops.fn[op])(m, t);
764 done(key);
765 ++attempts;
766 ++committed_attempts;
767 if (newtup) {
768 renew_neighbor_tuples(m, name, newtup.value());
769 ++successes;
770 } else {
771 on_fail(m, name, t);
772 ++failures;
773 }
774 }
775 candidates.clear();
776 keys.clear();
777 m_stats.serial_tail_seconds +=
778 std::chrono::duration<double>(Clock::now() - inline_start).count();
779 }
780
781 // A round in which nothing got as far as an attempt, while operations waited: what
782 // they wait on is still queued and the mesh did not change, so they would wait
783 // forever. Fail them as the serial queue fails a wait (see must_wait). With no
784 // attempt there was no success, so everything in `next` waited.
785 if (committed_attempts == committed_before && !next.ops.empty()) {
786 for (size_t i = 0; i < next.ops.size(); ++i) {
787 done(keyed ? next.keys[i] : 0);
788 m_wait_defects.fetch_add(1, std::memory_order_relaxed);
789 on_fail(m, *ops.name[next.ops[i].first], next.ops[i].second);
790 ++failures;
791 ++attempts;
792 }
793 std::deque<Cand>().swap(next.ops);
794 std::deque<uint64_t>().swap(next.keys);
795 }
796
797 const auto round_commit = Clock::now() - commit_start;
798 commit_time += round_commit;
799
800 // 4. Next round: what the successes renewed and what waited, once each, filtered as
801 // the queue would.
802 take_unique_renewed(m, ops, next);
803 candidates = std::move(next.ops);
804 keys = std::move(next.keys);
805 next.ops = {};
806 next.keys = {};
807 }
808
809 cnt_success = int(successes);
810 cnt_fail = int(failures);
811 logger().info("executed: {} | success / fail: {} / {}", attempts, successes, failures);
812 const auto secs = [](Clock::duration d) {
813 return std::chrono::duration<double>(d).count();
814 };
815 logger().info(
816 " screened: {} rounds, {} of {} attempts got past the screen, {} waits; screen "
817 "{:.4}s, commit {:.4}s",
818 rounds,
819 committed_attempts,
820 attempts,
821 m_waits.load() - waits_before,
822 secs(screen_time),
823 secs(commit_time));
824 // Ring-acquisition failures are a property of the commits, not of the screen refusals.
825 log_contention(committed_attempts);
826 check_queued_drained();
827 return true;
828 }
829
834 {
835 if (queue_key) {
836 const size_t left = m_queued.size();
837 if (left > 0 || m_untracked_done > 0) {
838 logger().warn(
839 "queue_key bookkeeping: {} keys still counted as queued after the pass, {} "
840 "finished operations were never counted",
841 left,
842 m_untracked_done);
843 }
844 }
845 m_queued.clear();
846 }
847
857 void take_unique_renewed(const AppMesh& m, const OpTable& ops, WaitSink& s)
858 {
859 const bool keyed = bool(queue_key);
860 const size_t n = s.ops.size();
861 if (n == 0) return;
862 assert(n < std::numeric_limits<uint32_t>::max());
863 // The kind of simplex each operation is named after.
864 enum : uint8_t { kOther, kVertex, kEdge, kFace, kTet };
865 std::vector<uint8_t> kind(ops.name.size(), kOther);
866 for (size_t o = 0; o < ops.name.size(); ++o) {
867 const Op& name = *ops.name[o];
868 if (name.rfind("vertex", 0) == 0) {
869 kind[o] = kVertex;
870 } else if (name.rfind("edge", 0) == 0) {
871 kind[o] = kEdge;
872 } else if (name.rfind("face", 0) == 0) {
873 kind[o] = kFace;
874 } else if (name.rfind("tet", 0) == 0) {
875 kind[o] = kTet;
876 }
877 }
878 // (rank, ids...) and the entry's index; a dropped entry has the largest rank, so it sorts
879 // last. Ids are 32-bit: no mesh here comes near 2^32 vertices or cells.
880 using Key = std::array<uint32_t, 4>;
881 constexpr uint32_t kDropped = std::numeric_limits<uint32_t>::max();
882 const auto id = [](size_t v) {
883 assert(v < std::numeric_limits<uint32_t>::max());
884 return uint32_t(v);
885 };
886 std::vector<std::pair<Key, uint32_t>> keyed_entries(n);
887 threading::dynamic_parallel_for(n, std::max(1, num_threads), 256, [&](size_t b, size_t e) {
888 for (size_t i = b; i < e; ++i) {
889 const auto& [o, t] = s.ops[i];
890 Key k = {{kDropped, 0, 0, 0}};
891 if (t.is_valid(m) && should_renew(priority(m, *ops.name[o], t))) {
892 k = {{o, kDropped, uint32_t(i), 0}};
893 switch (kind[o]) {
894 case kVertex: k = {{o, id(t.vid(m)), 0, 0}}; break;
895 case kEdge: k = {{o, id(t.vid(m)), id(t.switch_vertex(m).vid(m)), 0}}; break;
896 case kTet:
897 if constexpr (std::is_base_of<TetMesh, AppMesh>::value) {
898 k = {{o, id(t.tid(m)), 0, 0}};
899 }
900 break;
901 case kFace: {
902 std::array<size_t, 3> v;
903 if constexpr (std::is_base_of<TetMesh, AppMesh>::value) {
904 const auto fv = m.get_face_vertices(t);
905 v = {{fv[0].vid(m), fv[1].vid(m), fv[2].vid(m)}};
906 } else {
907 v = m.oriented_tri_vids(t);
908 }
909 std::sort(v.begin(), v.end());
910 k = {{o, id(v[0]), id(v[1]), id(v[2])}};
911 break;
912 }
913 default: break;
914 }
915 }
916 keyed_entries[i] = {k, uint32_t(i)};
917 }
918 });
919 std::sort(keyed_entries.begin(), keyed_entries.end());
920 // The first entry of each key -- the earliest renewed -- stays.
921 std::vector<uint8_t> kept(n, 0);
922 for (size_t j = 0; j < n && keyed_entries[j].first[0] != kDropped; ++j) {
923 if (j > 0 && keyed_entries[j].first == keyed_entries[j - 1].first) continue;
924 kept[keyed_entries[j].second] = 1;
925 }
926 std::vector<std::pair<Key, uint32_t>>().swap(keyed_entries);
927 size_t w = 0;
928 for (size_t i = 0; i < n; ++i) {
929 if (!kept[i]) {
930 if (keyed && s.keys[i] != 0 && !m_queued.remove(s.keys[i])) ++m_untracked_done;
931 continue;
932 }
933 if (w != i) {
934 s.ops[w] = std::move(s.ops[i]);
935 if (keyed) s.keys[w] = s.keys[i];
936 }
937 ++w;
938 }
939 s.ops.resize(w);
940 s.ops.shrink_to_fit();
941 if (keyed) {
942 s.keys.resize(w);
943 s.keys.shrink_to_fit();
944 }
945 }
946
951 bool execute(AppMesh& m, const std::function<void(const Push&)>& seed, WaitSink* sink = nullptr)
952 {
953 if constexpr (
954 std::is_base_of<TetMesh, AppMesh>::value || std::is_base_of<TriMesh, AppMesh>::value) {
955 const bool live_stop =
956 stopping_criterion_checking_frequency != std::numeric_limits<size_t>::max();
957 if (sink == nullptr && screen_before_commit && policy != ExecutionPolicy::kSeq &&
958 !live_stop) {
959 return run_screened(m, seed);
960 }
961 }
962 // The queue holds an operation's INDEX rather than its name. edit_operation_maps is a
963 // std::map, so iterating it yields the names in lexicographic order and an index is
964 // exactly a name's rank among them -- comparing indices is therefore identical to
965 // comparing the strings, which is what keeps the queue order, and so the output,
966 // bit-for-bit unchanged. What it buys is that a queue element is now trivially
967 // copyable: a heap sift moves 8 bytes instead of a std::string, and a tie on the
968 // priority is an integer compare instead of a string compare. It also turns the
969 // per-operation dispatch from a string-keyed std::map lookup into an index.
970 using OpId = uint32_t;
971 // priority, op index, #retries, tuple, queue_key. A struct rather than a std::tuple so the
972 // two 32-bit fields share a word: std::tuple lays its members out in reverse and pads each
973 // one to the Tuple's alignment, which costs 8 bytes per queued operation. The ordering
974 // is the one the std::tuple had -- (priority, op, tuple, retry, key), lexicographic -- so
975 // the pop order is unchanged. The key compares last, after the tuple, and two elements
976 // with the same tuple have the same key.
977 // What a parallel task may still take from the storage this round.
978 struct SlotBudget
979 {
980 size_t cells = 0;
981 size_t verts = 0;
982 bool stopped = false; // ended its round for want of slots, queue not drained
983 bool progressed = false; // ran at least one operation this round
984 };
985 struct Elem
986 {
987 double weight = 0;
988 OpId op = 0;
989 uint32_t retry = 0;
990 Tuple tup;
991 uint64_t key = 0; // queue_key, 0 when untracked
992
993 Elem() = default;
994 Elem(double w, OpId o, const Tuple& t, uint32_t r, uint64_t k)
995 : weight(w)
996 , op(o)
997 , retry(r)
998 , tup(t)
999 , key(k)
1000 {}
1001
1002 // A member, not a hidden friend: a local class cannot define friend functions.
1003 bool operator<(const Elem& b) const
1004 {
1005 return std::tie(weight, op, tup, retry, key) <
1006 std::tie(b.weight, b.op, b.tup, b.retry, b.key);
1007 }
1008 };
1009 // Each task owns its queue outright -- it is seeded before any thread starts, and the
1010 // task both pops from it and pushes its renewed operations back into it -- so those need
1011 // no lock. `final_queue` is the one that genuinely crosses threads: tasks push retry
1012 // overflow into it while running, and it is drained after the barrier. Both are
1013 // std::priority_queue with the same comparator underneath, so pop order is unchanged.
1016
1017 const OpTable table(edit_operation_maps);
1018 const auto& op_name = table.name;
1019 const auto& op_fn = table.fn;
1020 const auto id_of = [&table](const Op& name) -> OpId { return table.rank(name); };
1021
1022 std::atomic<bool> stop(false);
1023 cnt_success = 0;
1024 cnt_fail = 0;
1025
1026 // queue_key bookkeeping (see must_wait). `track` when an element is queued, `done` when
1027 // it is tried or dropped; both no-ops for the untracked key 0.
1028 if (!sink) m_queued.clear();
1029 std::atomic<size_t> untracked_done(0);
1030 const auto key_of = [&](const Op& op, const Tuple& e) -> uint64_t {
1031 return queue_key ? queue_key(m, op, e) : 0;
1032 };
1033 const auto track = [&](const uint64_t key) {
1034 if (key != 0) m_queued.add(key);
1035 };
1036 const auto done = [&](const uint64_t key) {
1037 if (key != 0 && !m_queued.remove(key)) {
1038 untracked_done.fetch_add(1, std::memory_order_relaxed);
1039 }
1040 };
1041
1042 // Whether anything actually watches the success count *while the pass runs*. When no
1043 // stopping criterion is configured -- the case for every tetwild/triwild/simwild pass --
1044 // nobody does, and the counters can be accumulated per task and folded in at the end
1045 // instead of hammering one shared cache line from every thread on every operation.
1046 const bool track_live_success =
1047 stopping_criterion_checking_frequency != std::numeric_limits<size_t>::max();
1048 std::atomic<size_t> live_success(0);
1049
1050 std::vector<LocalQueue> queues(num_threads);
1051 SharedQueue final_queue;
1052
1053 // Contention accounting. Everything here is either a per-task local folded in once or a
1054 // write to the task's own slot, so it adds nothing to the inner loop. It answers the
1055 // two questions the pass could not previously be asked: how often ring acquisition
1056 // loses a race, and how much of a "parallel" pass is really the serial drain.
1057 PassStats st{};
1058 std::atomic<size_t> lock_failures(0);
1059 std::atomic<size_t> overflowed(0);
1060 std::vector<double> task_seconds(queues.size(), 0.);
1061
1062 // Per-task tallies folded into the shared counters once, on the way out. The guard is
1063 // RAII rather than a line at the bottom because run_single_queue has early returns.
1064 //
1065 // DECLARED HERE, NOT INSIDE THE LAMBDA, to work around an Apple clang 17 codegen bug.
1066 // A local class declared inside a GENERIC lambda (one with an `auto` parameter, so its
1067 // operator() is a template) gets its destructor emitted as an undefined reference and
1068 // never defined, at every optimization level including -O0. The link then fails with
1069 // Undefined symbols: ... ::'lambda'(auto&, int)::operator()<...>
1070 // ::CountFlusher::~CountFlusher()
1071 // referenced from every translation unit that instantiates a pass -- 26 references
1072 // across 6 archives, defined nowhere. Homebrew clang 22 compiles the same code
1073 // correctly, which is why CI does not see this.
1074 //
1075 // Hoisting the class one scope out is enough: it is still local to operator(), which is
1076 // itself a member of a class template, and that instantiates fine. Nothing else changes
1077 // -- `counts` is still constructed per task inside the lambda, so the RAII flush and its
1078 // ordering are identical.
1079 struct CountFlusher
1080 {
1081 std::atomic_int& success_total;
1082 std::atomic_int& fail_total;
1083 std::atomic<size_t>& lock_failure_total;
1084 std::atomic<size_t>& overflow_total;
1085 int success = 0;
1086 int fail = 0;
1087 size_t lock_failure = 0;
1088 size_t overflow = 0;
1089 ~CountFlusher()
1090 {
1091 success_total.fetch_add(success, std::memory_order_relaxed);
1092 fail_total.fetch_add(fail, std::memory_order_relaxed);
1093 lock_failure_total.fetch_add(lock_failure, std::memory_order_relaxed);
1094 overflow_total.fetch_add(overflow, std::memory_order_relaxed);
1095 }
1096 };
1097
1098 // `serial`: no other thread is running operations on the mesh, and Q is popped by this
1099 // task alone and in priority order -- the serial policy, and the serial drain after a
1100 // parallel pass. The storage may then grow between operations (see
1101 // TetMesh::reserve_free_slots), never inside a parallel task; and a must_wait that says
1102 // true is a defect there.
1103 //
1104 // `budget`: the slots a parallel task may still take this round (see the round loop
1105 // below); null when the task may grow the storage instead.
1106 //
1107 // `carry`: where the deferred operations go when Q runs dry, for a task that has more
1108 // partitions to take (see the tasks below); null to retry them here until they succeed
1109 // or overflow.
1110 auto run_single_queue = [&](auto& Q,
1111 int task_id,
1112 const bool serial,
1113 SlotBudget* budget,
1114 std::vector<Elem>* carry) {
1115 CountFlusher counts{cnt_success, cnt_fail, lock_failures, overflowed};
1116
1117 Elem ele_in_queue;
1118 // Operations that lost a race for their ring wait here rather than going straight
1119 // back into the live queue. Pushing them back immediately is a spin: the element was
1120 // just popped as the queue's maximum, `retry` is the last tie-break key in Elem, so
1121 // the requeued copy compares strictly greater than what was popped and nothing else
1122 // was added -- it comes right back off the top and is retried against a conflict that
1123 // has had no time to clear. Deferring lets every other operation this task owns run
1124 // first, which is both useful work and the delay the conflict needs.
1125 std::vector<Elem> second_chance;
1126 // Operations popped since the deferred list was last given back to the queue.
1127 // Waiting for the queue to drain completely can mean thousands of operations, by
1128 // which time the mesh around a deferred operation has moved and its work has to be
1129 // rediscovered. A bounded window gives the conflict time to clear without letting
1130 // the operation go stale. 0 = only when the queue empties.
1131 size_t since_refill = 0;
1132 const auto refill = [&] {
1133 for (auto& e : second_chance) {
1134 Q.emplace(std::move(e));
1135 }
1136 second_chance.clear();
1137 since_refill = 0;
1138 };
1139 for (;;) {
1140 if (!second_chance.empty() && deferral_window > 0 &&
1141 since_refill >= deferral_window) {
1142 refill();
1143 }
1144 if (!Q.try_pop(ele_in_queue)) {
1145 if (second_chance.empty()) {
1146 break;
1147 }
1148 if (carry != nullptr) {
1149 // Retried after the task's next partition rather than right away: a
1150 // conflict that has had a whole partition's work to clear rarely
1151 // remains, where back-to-back retries send it to final_queue.
1152 for (auto& e : second_chance) carry->push_back(std::move(e));
1153 second_chance.clear();
1154 break;
1155 }
1156 // Queue exhausted: the deferred operations have now had everything else
1157 // run ahead of them, so give them another go. `retry` still increments on
1158 // each attempt and still overflows to final_queue at max_retry_limit, so
1159 // this terminates after at most that many rounds.
1160 refill();
1161 std::this_thread::yield();
1162 continue;
1163 }
1164 ++since_refill;
1165 auto& [weight, op, retry, tup, key] = ele_in_queue;
1166 // Without the ring yet, so other tasks may be changing this cell: a cheap filter
1167 // only (is_valid_unlocked), repeated in full once the ring is held.
1168 if (!tup.is_valid_unlocked(m)) {
1169 done(key);
1170 continue;
1171 }
1172
1173 std::vector<Elem> renewed_elements;
1174 {
1175 auto locked_vid = lock_vertices(
1176 m,
1177 tup,
1178 task_id); // Note that returning `Tuples` would be invalid.
1179 if (!locked_vid) {
1180 counts.lock_failure++;
1181 retry++;
1182 if (retry < max_retry_limit) {
1183 second_chance.push_back(ele_in_queue);
1184 } else {
1185 retry = 0;
1186 counts.overflow++;
1187 final_queue.emplace(ele_in_queue);
1188 }
1189 continue;
1190 }
1191 if (tup.is_valid(m)) {
1192 const Op& op_str = *op_name[op];
1193 if (!is_weight_up_to_date(
1194 m,
1195 std::tuple<double, Op, Tuple>(weight, op_str, tup))) {
1196 done(key);
1197 operation_cleanup(m);
1198 continue;
1199 } // this can encode, in qslim, recompute(energy) == weight.
1200 if (must_wait && must_wait(m, op_str, tup)) {
1201 if (sink) {
1202 // Next round, still queued.
1203 operation_cleanup(m);
1204 m_waits.fetch_add(1, std::memory_order_relaxed);
1205 std::lock_guard<std::mutex> lock(sink->mutex);
1206 sink->ops.emplace_back(op, tup);
1207 if (queue_key) sink->keys.push_back(key);
1208 continue;
1209 }
1210 if (serial) {
1211 m_wait_defects.fetch_add(1, std::memory_order_relaxed);
1212 done(key);
1213 on_fail(m, op_str, tup);
1214 counts.fail++;
1215 operation_cleanup(m);
1216 continue;
1217 }
1218 // Set aside exactly like a lost lock race, above.
1219 operation_cleanup(m);
1220 m_waits.fetch_add(1, std::memory_order_relaxed);
1221 retry++;
1222 if (retry < max_retry_limit) {
1223 second_chance.push_back(ele_in_queue);
1224 } else {
1225 retry = 0;
1226 counts.overflow++;
1227 final_queue.emplace(ele_in_queue);
1228 }
1229 continue;
1230 }
1231 if (serial) {
1232 m.reserve_free_slots(m.cell_slot_bound(tup), 1);
1233 } else if (budget != nullptr) {
1234 // The ring is locked, so the stars the bound reads are stable.
1235 if (m.cell_slot_bound(tup) > budget->cells || budget->verts < 1) {
1236 // Out of this round's slots: hand the operation back untouched,
1237 // with everything deferred, and end the task's round.
1238 operation_cleanup(m);
1239 Q.emplace(ele_in_queue);
1240 refill();
1241 budget->stopped = true;
1242 return;
1243 }
1244 }
1245 const auto usage_before = AppMesh::slot_usage_of_this_thread();
1246 auto newtup = (*op_fn[op])(m, tup);
1247 done(key);
1248 if (budget != nullptr) {
1249 const auto usage = AppMesh::slot_usage_of_this_thread();
1250 budget->cells -= usage.cells - usage_before.cells;
1251 budget->verts -= usage.verts - usage_before.verts;
1252 budget->progressed = true;
1253 }
1254 std::vector<std::pair<Op, Tuple>> renewed_tuples;
1255 if (newtup) {
1256 renewed_tuples = renew_neighbor_tuples(m, op_str, newtup.value());
1257 counts.success++;
1258 if (track_live_success) {
1259 live_success.fetch_add(1, std::memory_order_relaxed);
1260 }
1261 } else {
1262 on_fail(m, op_str, tup);
1263 counts.fail++;
1264 }
1265 // Tracked here, with the ring still locked, although the elements are
1266 // queued only after it is released: in between they count as queued.
1267 for (const auto& [o, e] : renewed_tuples) {
1268 auto val = priority(m, o, e);
1269 if (should_renew(val)) {
1270 const uint64_t k = key_of(o, e);
1271 track(k);
1272 renewed_elements.emplace_back(val, id_of(o), e, 0, k);
1273 }
1274 }
1275 } else {
1276 done(key);
1277 }
1278 operation_cleanup(m); // Maybe use RAII
1279 }
1280 for (auto& e : renewed_elements) {
1281 Q.emplace(e);
1282 }
1283
1284 if (stop.load(std::memory_order_acquire)) {
1285 return;
1286 }
1287 if (track_live_success && live_success.load(std::memory_order_relaxed) >
1288 stopping_criterion_checking_frequency) {
1289 if (stopping_criterion(m)) {
1290 stop.store(true);
1291 return;
1292 }
1293 }
1294 }
1295 };
1296
1297 if (policy == ExecutionPolicy::kSeq) {
1298 seed([&](const Op& op, const Tuple& e, uint64_t) {
1299 if (!e.is_valid(m)) {
1300 return;
1301 }
1302 const uint64_t k = key_of(op, e);
1303 track(k);
1304 final_queue.emplace(priority(m, op, e), id_of(op), e, 0, k);
1305 });
1306 run_single_queue(final_queue, 0, /*serial=*/true, nullptr, nullptr);
1307 } else {
1308 seed([&](const Op& op, const Tuple& e, const uint64_t counted) {
1309 if (!e.is_valid(m)) {
1310 if (sink) done(counted);
1311 return;
1312 }
1313 // A screened commit's candidates were counted by the screened call, under the
1314 // key it hands over; anything else is counted here.
1315 const uint64_t k = sink ? counted : key_of(op, e);
1316 if (!sink) track(k);
1317 // A mesh may cut itself into more partitions than there are threads; see the
1318 // tasks below.
1319 const size_t part = get_partition_id(m, e);
1320 if (part >= queues.size()) queues.resize(part + 1);
1321 queues[part].emplace(priority(m, op, e), id_of(op), e, 0, k);
1322 });
1323 // Comment out parallel: work on serial first.
1324 using clock = std::chrono::steady_clock;
1325 const auto t_parallel = clock::now();
1326 // Rounds. The storage cannot grow while the tasks run, so each round starts at a
1327 // serial point that grows it by a headroom split into per-task budgets; a task runs
1328 // an operation only if its budget covers the operation's slot bound, and is charged
1329 // what the operation actually took. A task that cannot afford its next operation
1330 // hands it back and ends its round. No operation is ever refused for want of slots,
1331 // and the storage only ever needs to be a quarter larger than the mesh.
1332 //
1333 // A task that ends a round without having run anything had a single operation
1334 // larger than its share; its share doubles until it fits, so every round but the
1335 // last makes progress and the loop terminates.
1336 //
1337 // A task does not own a partition: it takes the next one no task has taken yet and
1338 // drains it, then the next, until none is left (or its budget runs out). With one
1339 // partition per thread that is the one-to-one assignment it always was. A mesh cut
1340 // into more partitions than threads (ShortestEdgeCollapse::partition_mesh) is
1341 // balanced by it: partitions of equal size can hold very different amounts of work,
1342 // and a task that drew light ones goes on to take more.
1343 const size_t n_tasks = size_t(std::max(1, num_threads));
1344 std::atomic<size_t> next_queue{0};
1345 std::vector<SlotBudget> budgets(n_tasks);
1346 std::vector<unsigned> boost(n_tasks, 0);
1348 for (size_t round = 0;; ++round) {
1349 // A quarter of the mesh per round, or whatever the storage already has free if
1350 // that is more -- a mesh that still preallocates never runs more rounds than
1351 // its headroom forces.
1352 const size_t cell_room =
1353 std::max(m.cell_capacity() / 4, m.cell_storage_capacity() - m.cell_capacity());
1354 const size_t vert_room =
1355 std::max(m.vert_capacity() / 4, m.vert_storage_capacity() - m.vert_capacity());
1356 const size_t cell_share = std::max<size_t>(4096, cell_room / n_tasks);
1357 const size_t vert_share = std::max<size_t>(1024, vert_room / n_tasks);
1358 size_t cells = 0, verts = 0;
1359 for (size_t t = 0; t < n_tasks; ++t) {
1360 budgets[t] = SlotBudget{cell_share << boost[t], vert_share << boost[t]};
1361 cells += budgets[t].cells;
1362 verts += budgets[t].verts;
1363 }
1364 m.reserve_free_slots(cells, verts);
1365 // Every round goes through all the partitions again: an emptied one is passed
1366 // over at once, and one a task left for want of slots is taken up again.
1367 next_queue.store(0, std::memory_order_relaxed);
1368 for (int task_id = 0; task_id < n_tasks; task_id++) {
1369 tg.run([&, task_id] {
1370 const auto t0 = clock::now();
1371 SlotBudget& budget = budgets[task_id];
1372 const auto done_for_round = [&] {
1373 return budget.stopped || stop.load(std::memory_order_acquire);
1374 };
1375 // Operations deferred in one partition are retried in the next one this
1376 // task takes, after its own operations; see run_single_queue.
1377 std::vector<Elem> carry;
1378 size_t last = 0;
1379 for (size_t part; (part = next_queue.fetch_add(1)) < queues.size();) {
1380 for (auto& e : carry) queues[part].emplace(std::move(e));
1381 carry.clear();
1382 last = part;
1383 run_single_queue(
1384 queues[part],
1385 task_id,
1386 /*serial=*/false,
1387 &budget,
1388 &carry);
1389 if (done_for_round()) break;
1390 }
1391 // None left to take: retry the deferred operations here, until they
1392 // succeed or overflow to final_queue. The partition is this task's for the
1393 // round, and the next round finds there whatever this one leaves.
1394 if (!carry.empty()) {
1395 for (auto& e : carry) queues[last].emplace(std::move(e));
1396 carry.clear();
1397 if (!done_for_round()) {
1398 run_single_queue(
1399 queues[last],
1400 task_id,
1401 /*serial=*/false,
1402 &budget,
1403 nullptr);
1404 }
1405 }
1406 // Each task writes only its own slot.
1407 task_seconds[task_id] +=
1408 std::chrono::duration<double>(clock::now() - t0).count();
1409 });
1410 }
1411 tg.wait();
1412 bool again = false;
1413 for (size_t t = 0; t < n_tasks; ++t) {
1414 if (!budgets[t].stopped) continue;
1415 again = true;
1416 if (!budgets[t].progressed) boost[t] = std::min(boost[t] + 1, 24u);
1417 }
1418 if (!again) break;
1419 st.rounds = round + 2;
1420 }
1421 st.parallel_seconds = std::chrono::duration<double>(clock::now() - t_parallel).count();
1422 st.final_queue_size = final_queue.size();
1423
1424 logger().debug("Parallel Complete, remains element {}", final_queue.size());
1425
1426 const auto t_tail = clock::now();
1427 run_single_queue(final_queue, 0, /*serial=*/true, nullptr, nullptr);
1428 st.serial_tail_seconds = std::chrono::duration<double>(clock::now() - t_tail).count();
1429 }
1430
1431 st.lock_failures = lock_failures.load(std::memory_order_relaxed);
1432 st.overflowed = overflowed.load(std::memory_order_relaxed);
1433 if (!task_seconds.empty()) {
1434 const auto mm = std::minmax_element(task_seconds.begin(), task_seconds.end());
1435 st.idlest_task_seconds = *mm.first;
1436 st.busiest_task_seconds = *mm.second;
1437 }
1438
1439 if (sink) {
1440 m_stats += st; // a round of a screened call: the call adds its rounds up
1441 } else {
1442 m_stats = st;
1443 }
1444
1445 if (!sink) {
1446 logger().info(
1447 "executed: {} | success / fail: {} / {}",
1448 (int)cnt_success + (int)cnt_fail,
1449 (int)cnt_success,
1450 (int)cnt_fail);
1451 if (st.rounds > 1) {
1452 logger().info(
1453 " parallel region ran in {} rounds (storage grown between them)",
1454 st.rounds);
1455 }
1456 log_contention();
1457 }
1458 if (sink) {
1459 // The screened call checks the counts at its end.
1460 m_untracked_done += untracked_done.load();
1461 return true;
1462 }
1463 // Every tracked element queued in this call was tried or dropped, unless the stopping
1464 // criterion ended the call early. Anything else is a bookkeeping defect, and is_queued()
1465 // may have answered wrongly during the call.
1466 if (queue_key && !stop.load()) {
1467 const size_t left = m_queued.size();
1468 const size_t untracked = untracked_done.load();
1469 if (left > 0 || untracked > 0) {
1470 logger().warn(
1471 "queue_key bookkeeping: {} keys still counted as queued after the pass, {} "
1472 "finished elements were never counted",
1473 left,
1474 untracked);
1475 }
1476 }
1477 m_queued.clear();
1478 return true;
1479 }
1480
1481public:
1482 int get_cnt_success() const { return cnt_success; }
1483 int get_cnt_fail() const { return cnt_fail; }
1484
1495 {
1498 size_t lock_failures = 0;
1500 size_t overflowed = 0;
1502 size_t final_queue_size = 0;
1505 double parallel_seconds = 0.;
1506 double serial_tail_seconds = 0.;
1510 double busiest_task_seconds = 0.;
1511 double idlest_task_seconds = 0.;
1515 size_t rounds = 1;
1516
1517 PassStats& operator+=(const PassStats& o)
1518 {
1519 lock_failures += o.lock_failures;
1520 overflowed += o.overflowed;
1521 final_queue_size += o.final_queue_size;
1522 parallel_seconds += o.parallel_seconds;
1523 serial_tail_seconds += o.serial_tail_seconds;
1524 busiest_task_seconds += o.busiest_task_seconds;
1525 idlest_task_seconds += o.idlest_task_seconds;
1526 rounds = std::max(rounds, o.rounds);
1527 return *this;
1528 }
1529 };
1530 const PassStats& stats() const { return m_stats; }
1531
1532private:
1537 void log_contention(size_t executed_ops = std::numeric_limits<size_t>::max()) const
1538 {
1539 if (policy == ExecutionPolicy::kSeq || !logger().should_log(spdlog::level::debug)) {
1540 return;
1541 }
1542 const int executed = executed_ops != std::numeric_limits<size_t>::max()
1543 ? int(executed_ops)
1544 : (int)cnt_success + (int)cnt_fail;
1545 const double total = m_stats.parallel_seconds + m_stats.serial_tail_seconds;
1546 logger().debug(
1547 " contention: {} ring-acquisition failures over {} executed ops ({:.2f} per op); "
1548 "{} overflowed to the serial queue ({} queued at the barrier)",
1549 m_stats.lock_failures,
1550 executed,
1551 executed > 0 ? double(m_stats.lock_failures) / executed : 0.,
1552 m_stats.overflowed,
1553 m_stats.final_queue_size);
1554 logger().debug(
1555 " time: {:.4}s parallel + {:.4}s serial tail ({:.1f}% of the pass); busiest task "
1556 "{:.4}s, idlest {:.4}s",
1557 m_stats.parallel_seconds,
1558 m_stats.serial_tail_seconds,
1559 total > 0. ? 100. * m_stats.serial_tail_seconds / total : 0.,
1560 m_stats.busiest_task_seconds,
1561 m_stats.idlest_task_seconds);
1562 }
1563
1564 // Totals for the whole pass. Written once per task, at the end -- see CountFlusher.
1565 std::atomic_int cnt_success = 0;
1566 std::atomic_int cnt_fail = 0;
1567 PassStats m_stats;
1568
1574 {
1575 static constexpr size_t n_stripes = 1024;
1576 struct alignas(64) Stripe
1577 {
1579 std::unordered_map<uint64_t, int> count;
1580 };
1581 std::unique_ptr<Stripe[]> m_stripes = std::make_unique<Stripe[]>(n_stripes);
1582 Stripe& stripe(const uint64_t key) const
1583 {
1584 return m_stripes[std::hash<uint64_t>{}(key) % n_stripes];
1585 }
1586
1587 public:
1588 void add(const uint64_t key)
1589 {
1590 Stripe& s = stripe(key);
1591 std::lock_guard<threading::spin_mutex> lock(s.mutex);
1592 ++s.count[key];
1593 }
1595 bool remove(const uint64_t key)
1596 {
1597 Stripe& s = stripe(key);
1598 std::lock_guard<threading::spin_mutex> lock(s.mutex);
1599 const auto it = s.count.find(key);
1600 if (it == s.count.end()) return false;
1601 if (--it->second == 0) s.count.erase(it);
1602 return true;
1603 }
1604 bool contains(const uint64_t key) const
1605 {
1606 Stripe& s = stripe(key);
1607 std::lock_guard<threading::spin_mutex> lock(s.mutex);
1608 return s.count.find(key) != s.count.end();
1609 }
1611 size_t size() const
1612 {
1613 size_t n = 0;
1614 for (size_t i = 0; i < n_stripes; ++i) n += m_stripes[i].count.size();
1615 return n;
1616 }
1618 void clear()
1619 {
1620 for (size_t i = 0; i < n_stripes; ++i) m_stripes[i].count.clear();
1621 }
1622 };
1623 QueuedCount m_queued;
1624 std::atomic<size_t> m_waits = 0;
1625 std::atomic<size_t> m_wait_defects = 0;
1628 size_t m_untracked_done = 0;
1629};
1630} // namespace wmtk
Definition ExecutionScheduler.hpp:1574
void clear()
Only while no thread is queueing or finishing elements.
Definition ExecutionScheduler.hpp:1618
bool remove(const uint64_t key)
False if key was not counted.
Definition ExecutionScheduler.hpp:1595
size_t size() const
Keys still counted. Only while no thread is queueing or finishing elements.
Definition ExecutionScheduler.hpp:1611
A pass's candidate operations, compact and consumed as they are queued.
Definition ExecutionScheduler.hpp:61
void consume(F &&f)
Hand every entry to f(name, tuple), front to back, releasing them as it goes.
Definition ExecutionScheduler.hpp:70
void append(OpList &&other)
Move other's entries to the end of this list, releasing them from other as it goes.
Definition ExecutionScheduler.hpp:81
Definition concurrent_priority_queue.hpp:15
Definition serial_priority_queue.hpp:24
Definition spin_mutex.hpp:13
Definition task_group.hpp:210
std::set< Tet > keys(const Tets &T)
The tets as a set of sorted tets.
Definition volume_complex.cpp:41
The registered operations by rank. edit_operation_maps is a std::map, so iterating it yields the name...
Definition ExecutionScheduler.hpp:530
What the last pass cost in contention, as opposed to in work.
Definition ExecutionScheduler.hpp:1495
size_t final_queue_size
Size of that queue once every task had finished.
Definition ExecutionScheduler.hpp:1502
size_t lock_failures
Definition ExecutionScheduler.hpp:1498
double parallel_seconds
Definition ExecutionScheduler.hpp:1505
size_t overflowed
Operations that exhausted max_retry_limit and were pushed to the post-barrier queue.
Definition ExecutionScheduler.hpp:1500
double busiest_task_seconds
Definition ExecutionScheduler.hpp:1510
size_t rounds
Definition ExecutionScheduler.hpp:1515
Definition ExecutionScheduler.hpp:1577
Where a screened round collects the next round's candidates: the operations its successes renewed and...
Definition ExecutionScheduler.hpp:569
Definition ExecutionScheduler.hpp:123
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:265
size_t waits() const
Definition ExecutionScheduler.hpp:238
bool run_screened(AppMesh &m, const std::function< void(const Push &)> &seed)
Screened rounds; see screen_before_commit.
Definition ExecutionScheduler.hpp:576
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:204
void take_unique_renewed(const AppMesh &m, const OpTable &ops, WaitSink &s)
Leaves in s the valid, still-wanted operations, each (operation, simplex) once, with their queue keys...
Definition ExecutionScheduler.hpp:857
std::function< uint64_t(const AppMesh &, const Op &, const Tuple &)> queue_key
Optional order between queued operations, used by the split passes.
Definition ExecutionScheduler.hpp:233
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:196
size_t m_untracked_done
Definition ExecutionScheduler.hpp:1628
std::function< bool(AppMesh &, const Tuple &, int task_id)> lock_vertices
lock the vertices concerned depends on the operation
Definition ExecutionScheduler.hpp:157
std::function< void(const Op &, const Tuple &, uint64_t)> Push
Queues one candidate; what a seeder calls for each entry of its list.
Definition ExecutionScheduler.hpp:521
bool operator()(AppMesh &m, OpList< Tuple > &&operation_tuples)
As above for an OpList, which is released entry by entry while the queues are seeded,...
Definition ExecutionScheduler.hpp:506
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:149
size_t stopping_criterion_checking_frequency
Cumulative successful operations before stopping_criterion is first consulted.
Definition ExecutionScheduler.hpp:188
std::function< double(const AppMesh &, Op op, const Tuple &)> priority
Priority function (default to edge length)
Definition ExecutionScheduler.hpp:137
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:326
void check_queued_drained()
Definition ExecutionScheduler.hpp:833
bool screen_before_commit
Screen every operation with a dry run before committing any.
Definition ExecutionScheduler.hpp:319
bool operator()(AppMesh &m, std::vector< std::pair< Op, Tuple > > &&operation_tuples)
As above, but takes the operation list over and frees it as soon as the queues are seeded.
Definition ExecutionScheduler.hpp:493
void log_contention(size_t executed_ops=std::numeric_limits< size_t >::max()) const
Definition ExecutionScheduler.hpp:1537
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:477
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:289
bool execute(AppMesh &m, const std::function< void(const Push &)> &seed, WaitSink *sink=nullptr)
Definition ExecutionScheduler.hpp:951
std::function< bool(double)> should_renew
check on wheather new operations should be added to the priority queue
Definition ExecutionScheduler.hpp:143
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:132
std::function< bool(const AppMesh &)> stopping_criterion
Stopping Criterion based on the whole mesh For efficiency, not every time is checked....
Definition ExecutionScheduler.hpp:166