Wildmeshing Toolkit
Loading...
Searching...
No Matches
Classes | Public Types | Public Member Functions | Public Attributes | Private Types | Private Member Functions | Private Attributes | List of all members
wmtk::ExecutePass< AppMesh > Struct Template Reference

Classes

struct  OpTable
 The registered operations by rank. edit_operation_maps is a std::map, so iterating it yields the names in lexicographic order and an operation's index is its name's rank: comparing ranks is comparing names, which is what keeps the queue order unchanged. More...
 
struct  PassStats
 What the last pass cost in contention, as opposed to in work. More...
 
class  QueuedCount
 
struct  WaitSink
 Where a screened round collects the next round's candidates: the operations its successes renewed and the ones must_wait set aside, by rank (see OpTable), each with its queue key, which stays counted (keys is empty when the pass sets no queue_key). More...
 

Public Types

using Tuple = typename AppMesh::Tuple
 

Public Member Functions

bool is_queued (uint64_t key) const
 
size_t waits () const
 
size_t wait_defects () const
 
 ExecutePass (const ExecutionPolicy &policy_=ExecutionPolicy::kSeq)
 Construct a new Execute Pass object. It contains the name-to-operation map and the functions that define the rules for operations.
 
 ExecutePass (ExecutePass &)=delete
 
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 customizly defined for applications are applied.
 
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.
 
bool operator() (AppMesh &m, OpList< Tuple > &&operation_tuples)
 As above for an OpList, which is released entry by entry while the queues are seeded, so the list and the queue never both hold every candidate.
 
int get_cnt_success () const
 
int get_cnt_fail () const
 
const PassStats & stats () const
 

Public Attributes

std::map< Op, std::function< std::optional< std::vector< Tuple > >(AppMesh &, const Tuple &)> > edit_operation_maps
 A dictionary that registers names with operations.
 
std::function< double(const AppMesh &, Op op, const Tuple &)> priority
 Priority function (default to edge length)
 
std::function< bool(double)> should_renew = [](double) { return true; }
 check on wheather new operations should be added to the priority queue
 
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
 
std::function< bool(AppMesh &, const Tuple &, int task_id)> lock_vertices
 lock the vertices concerned depends on the operation
 
std::function< bool(const AppMesh &)> stopping_criterion
 Stopping Criterion based on the whole mesh For efficiency, not every time is checked. In serial, this may go over all the elements. For parallel, this involves synchronization. So there is a checking frequency.
 
size_t stopping_criterion_checking_frequency = std::numeric_limits<size_t>::max()
 Cumulative successful operations before stopping_criterion is first consulted.
 
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. This is in addition to calling tuple valid.
 
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
 
std::function< uint64_t(const AppMesh &, const Op &, const Tuple &)> queue_key
 Optional order between queued operations, used by the split passes.
 
std::function< bool(const AppMesh &, const Op &, const Tuple &)> must_wait
 
ExecutionPolicy policy
 
int num_threads = 1
 
size_t max_retry_limit = 10
 Attempts an operation gets at claiming its ring before it is handed to the serial queue drained after the barrier.
 
size_t deferral_window = 128
 Operations to run before handing deferred operations back to the queue, or 0 to wait until the queue empties.
 
bool screen_before_commit = true
 Screen every operation with a dry run before committing any.
 

Private Types

using Push = std::function< void(const Op &, const Tuple &, uint64_t)>
 Queues one candidate; what a seeder calls for each entry of its list.
 
using OpFn = std::function< std::optional< std::vector< Tuple > >(AppMesh &, const Tuple &)>
 

Private Member Functions

void operation_cleanup (AppMesh &m)
 
size_t get_partition_id (const AppMesh &m, const Tuple &e)
 
bool run_screened (AppMesh &m, const std::function< void(const Push &)> &seed)
 Screened rounds; see screen_before_commit.
 
void check_queued_drained ()
 
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; the ones left out are no longer queued.
 
bool execute (AppMesh &m, const std::function< void(const Push &)> &seed, WaitSink *sink=nullptr)
 
void log_contention (size_t executed_ops=std::numeric_limits< size_t >::max()) const
 

Private Attributes

std::atomic_int cnt_success = 0
 
std::atomic_int cnt_fail = 0
 
PassStats m_stats
 
QueuedCount m_queued
 
std::atomic< size_t > m_waits = 0
 
std::atomic< size_t > m_wait_defects = 0
 
size_t m_untracked_done = 0
 

Member Typedef Documentation

◆ Push

template<class AppMesh >
using wmtk::ExecutePass< AppMesh >::Push = std::function<void(const Op&, const Tuple&, uint64_t)>
private

Queues one candidate; what a seeder calls for each entry of its list.

The third argument is the candidate's queue key when the caller has already counted it (a screened round's commit, see WaitSink); every other seeder passes 0, and the key is computed and counted as the candidate is queued.

Constructor & Destructor Documentation

◆ ExecutePass()

template<class AppMesh >
wmtk::ExecutePass< AppMesh >::ExecutePass ( const ExecutionPolicy &  policy_ = ExecutionPolicy::kSeq)
inline

Construct a new Execute Pass object. It contains the name-to-operation map and the functions that define the rules for operations.

Note
the constructor is differentiated by the type of mesh, namingly wmtk::TetMesh or wmtk::TriMesh

Member Function Documentation

◆ check_queued_drained()

template<class AppMesh >
void wmtk::ExecutePass< AppMesh >::check_queued_drained ( )
inlineprivate

Every operation counted as queued during a call was tried or dropped by its end; anything left, or a finished operation that was never counted, means is_queued() may have answered wrongly during the call.

◆ execute()

template<class AppMesh >
bool wmtk::ExecutePass< AppMesh >::execute ( AppMesh &  m,
const std::function< void(const Push &)> &  seed,
WaitSink *  sink = nullptr 
)
inlineprivate

sink non-null: a screened round's commit. It logs nothing (the screened call does); the queue_key counts span the whole screened call, so they are neither reset nor counted here – the seeder hands over each candidate's counted key – and a waiting operation goes to the sink.

◆ log_contention()

template<class AppMesh >
void wmtk::ExecutePass< AppMesh >::log_contention ( size_t  executed_ops = std::numeric_limits<size_t>::max()) const
inlineprivate

Debug-level because it is per pass and there are many passes per iteration. Enable with the logger at debug to see whether contention is worth acting on. executed: the operations the contention is spread over; by default every attempt of the last call. A screened call passes the attempts that got past its screen.

◆ operator()() [1/2]

template<class AppMesh >
bool wmtk::ExecutePass< AppMesh >::operator() ( AppMesh &  m,
const std::vector< std::pair< Op, Tuple > > &  operation_tuples 
)
inline

Executes the operations for an application when the lambda function is invoked. The rules that are customizly defined for applications are applied.

Parameters
m
operation_tuplesa vector of pairs of operation's name and the Tuple to be operated on
Returns
true if finished successfully

◆ operator()() [2/2]

template<class AppMesh >
bool wmtk::ExecutePass< AppMesh >::operator() ( AppMesh &  m,
std::vector< std::pair< Op, Tuple > > &&  operation_tuples 
)
inline

As above, but takes the operation list over and frees it as soon as the queues are seeded.

The queues hold their own copy of every operation, so the list is dead weight from then on – and it is at its longest on the first pass after construction, which is also when the mesh is at its largest. Releasing it there is what keeps a pass from holding every candidate twice at the run's peak.

◆ take_unique_renewed()

template<class AppMesh >
void wmtk::ExecutePass< AppMesh >::take_unique_renewed ( const AppMesh &  m,
const OpTable &  ops,
WaitSink &  s 
)
inlineprivate

Leaves in s the valid, still-wanted operations, each (operation, simplex) once, with their queue keys; the ones left out are no longer queued.

Each entry's validity, priority and dedup key are computed in parallel – priority() is the expensive part. The key is the operation's rank and its simplex: the vertex ids, ordered for edges since a collapse is directed, sorted for faces; the tet id for tets. An operation named after none of these is never merged with another. The list is compacted in place.

◆ waits()

template<class AppMesh >
size_t wmtk::ExecutePass< AppMesh >::waits ( ) const
inline

Over every call on this executor: operations set aside by must_wait, and must_wait refusals in a serial run (defects, see above).

Member Data Documentation

◆ deferral_window

template<class AppMesh >
size_t wmtk::ExecutePass< AppMesh >::deferral_window = 128

Operations to run before handing deferred operations back to the queue, or 0 to wait until the queue empties.

Bounding this is what makes the second-chance list pay. A per-thread queue holds thousands of operations, so draining only at queue-empty means a deferred operation waits that long, by which time the mesh around it has moved, is_weight_up_to_date rejects it, and the work has to be rediscovered. That inflates the attempt count by roughly a fifth and eats most of the per-operation saving.

Measured at 16 threads, three reps, optimization wall time against the pre-deferral baseline, with attempts in brackets:

               103197 (base 23.0M)      101881 (base 48.8M)
 window 0       -2.2%  [28.5M]          -20.4%  [45.7M]
 window 32     -16.3%  [23.7M]          -21.6%  [43.7M]
 window 128    -17.9%  [23.1M]          -26.4%  [41.9M]
 window 512    -17.0%  [23.7M]          -25.5%  [42.7M]

128 is at or near best on both and keeps the attempt count closest to the baseline's. The result is not sharp between 32 and 512; it is sharp against 0.

◆ is_weight_up_to_date

template<class AppMesh >
std::function<bool(const AppMesh&, const std::tuple<double, Op, Tuple>& t)> wmtk::ExecutePass< AppMesh >::is_weight_up_to_date
Initial value:
= [](const AppMesh& m, const std::tuple<double, Op, Tuple>& t) {
assert(std::get<2>(t).is_valid(m));
return true;
}

Should Process drops some Tuple from being processed. For example, if the energy is out-dated. This is in addition to calling tuple valid.

◆ lock_vertices

template<class AppMesh >
std::function<bool(AppMesh&, const Tuple&, int task_id)> wmtk::ExecutePass< AppMesh >::lock_vertices
Initial value:
=
[](const AppMesh&, const Tuple&, int task_id) { return true; }

lock the vertices concerned depends on the operation

◆ m_untracked_done

template<class AppMesh >
size_t wmtk::ExecutePass< AppMesh >::m_untracked_done = 0
private

A screened call's finished operations that were not counted as queued (see check_queued_drained()); written by the calling thread only.

◆ max_retry_limit

template<class AppMesh >
size_t wmtk::ExecutePass< AppMesh >::max_retry_limit = 10

Attempts an operation gets at claiming its ring before it is handed to the serial queue drained after the barrier.

10 was swept and left alone. Measured on 128k-tet Thingi10K 103197 at 16 threads, three reps each, in µs per attempted operation:

immediate requeue (before the second-chance deferral below existed): 1: +17.0% 2: +7.2% 3: +6.2% 5: +4.6% 10: best 20: +6.2% with the deferral: 2: 2.170 3: 1.969 10: 1.974 (baseline at 10 was 2.527)

So under the old immediate-requeue behaviour the value mattered a lot – a low limit sent everything it stopped retrying to the serial queue, and that tail (23% of scheduler time at 10, 39% at 1) cost more than the spinning it avoided. With the deferral the retries are nearly free, the overflow pressure disappears, and 3 and 10 become indistinguishable. Left at 10 because nothing argues for moving it.

Below about 3 it still hurts: at 2 the overflow rate climbs enough to be visible again.

◆ on_fail

template<class AppMesh >
std::function<void(const AppMesh&, Op, const Tuple& t)> wmtk::ExecutePass< AppMesh >::on_fail
Initial value:
=
[](const AppMesh&, Op, const Tuple& t) {}

used to collect operations that are not finished and used for later re-execution

◆ priority

template<class AppMesh >
std::function<double(const AppMesh&, Op op, const Tuple&)> wmtk::ExecutePass< AppMesh >::priority
Initial value:
=
[](const AppMesh&, Op, const Tuple&) { return 0.; }

Priority function (default to edge length)

◆ queue_key

template<class AppMesh >
std::function<uint64_t(const AppMesh&, const Op&, const Tuple&)> wmtk::ExecutePass< AppMesh >::queue_key

Optional order between queued operations, used by the split passes.

queue_key names what an operation is about – for a split, its edge (edge_queue_key) – or returns 0 for "not tracked". A tracked operation is counted from the moment it is queued until it is tried, or dropped as invalid or out of date. Being set aside after a lost lock race does not end it. is_queued() reads that count, for any thread.

must_wait is asked after is_weight_up_to_date, with the operation's ring locked. True sets the operation aside exactly like a lost lock race: it is retried later in the pass and, after max_retry_limit attempts, handed to the serial queue drained after the barrier. A serial run – kSeq, or that serial queue – pops in priority order, so whatever an operation could wait on has been tried before it. There a must_wait that still says true is a defect: the operation fails (on_fail) and is counted in wait_defects() rather than being set aside forever.

In a screened pass (screen_before_commit) an operation that waits is not retried within the round: it stays counted and is a candidate of the next round, screened again. A round in which nothing got as far as an attempt while operations waited cannot make progress – what they wait on is still queued and the mesh has not changed – so there, too, every waiting operation is a defect: it fails and is counted in wait_defects(). A must_wait that orders the operations strictly never gets there, since the first operation in its order never waits.

Both empty, the default: nothing is counted and nothing waits.

◆ renew_neighbor_tuples

template<class AppMesh >
std::function<std::vector<std::pair<Op, Tuple> >(const AppMesh&, Op, const std::vector<Tuple>&)> wmtk::ExecutePass< AppMesh >::renew_neighbor_tuples
Initial value:
=
[](const AppMesh&, Op, const std::vector<Tuple>&) -> std::vector<std::pair<Op, Tuple>> {
return {};
}

renew neighboring Tuples after each operation depends on the operation

◆ screen_before_commit

template<class AppMesh >
bool wmtk::ExecutePass< AppMesh >::screen_before_commit = true

Screen every operation with a dry run before committing any.

When set, a parallel pass runs in rounds. Each round first dry-runs every candidate in parallel against the unchanging mesh, with no locks (see operation_dry_run()): everything an operation checks before its first change – the before-hook, valence, the choice of swap configuration – runs, and the operations refused there are reported through on_fail like any failure. Only the survivors go through the queues and ring locks, where they are attempted for real with every check repeated. The operations their successes renew are not queued straight away; they are the next round's candidates, screened the same way.

The point is that the expensive part of a parallel pass was claiming a two-ring of locks for every attempt, and ~98% of attempts on the challenging tetwild models are refused before their first change: those now cost a lock-free read, and the ring locks are taken by the ~2% that get as far as changing the mesh.

On by default for every parallel pass: every operation of the TetMesh and TriMesh registries honours operation_dry_run(), including the splits and vertex smoothing, where it screens out almost nothing but keeps all passes on one pipeline. One driver turns it off: the 2D collapse pass, where it costs more than it saves (TriOptimizerMeshCollapse.cpp). Not used by serial passes, which keep their single queue (and their output), nor by a pass with a live stopping criterion (qslim, shortest edge collapse): those collapse in global priority order up to an exact count, which rounds would reorder.

Memory: a round holds its candidates as (operation rank, tuple), compacts the survivors in place, and collects the next round's in deques, so a screened pass holds about what the queues of an unscreened one would.

◆ stopping_criterion

template<class AppMesh >
std::function<bool(const AppMesh&)> wmtk::ExecutePass< AppMesh >::stopping_criterion
Initial value:
= [](const AppMesh&) {
return false;
}

Stopping Criterion based on the whole mesh For efficiency, not every time is checked. In serial, this may go over all the elements. For parallel, this involves synchronization. So there is a checking frequency.

◆ stopping_criterion_checking_frequency

template<class AppMesh >
size_t wmtk::ExecutePass< AppMesh >::stopping_criterion_checking_frequency = std::numeric_limits<size_t>::max()

Cumulative successful operations before stopping_criterion is first consulted.

Despite the name this is a threshold, not a period: the count it is tested against is never reset, so once the pass has had this many successes the criterion is consulted after every subsequent operation. Both current users rely on exactly that – they set the criterion to return true and the threshold to the number of collapses needed to reach a target vertex count, making this a decimation counter that stops the pass on its first check. It is not a "check every N operations" knob, and writing a genuinely periodic criterion against it would evaluate that criterion on every operation forever after.

Left at the default, the criterion is never consulted at all, which is the case for every tetwild/triwild/simwild pass.

(There used to be a cnt_update member here that was incremented per success and reset inside the check, as if the threshold were a period. Nothing ever read it – the reset was on a branch the always-true criteria above never reach – so it was one more contended atomic on the hot path buying nothing, and it is gone.)


The documentation for this struct was generated from the following file: