Loading...
Searching...
No Matches
partitioner.hpp
1// reference:
2// - gomp: https://github.com/gcc-mirror/gcc/blob/master/libgomp/iter.c
3// - komp: https://github.com/llvm-mirror/openmp/blob/master/runtime/src/kmp_dispatch.cpp
4
5#pragma once
6
11
12namespace tf {
13
19enum class PartitionerType : int {
24};
25
32
33// ------------------------------------------------------------------------------------------------
34// IndexRangesPartitioner
35// ------------------------------------------------------------------------------------------------
36
40template <typename T, size_t N>
41class IndexRangesPartitioner {
42
43 public:
44
45 using R = tf::IndexRanges<T, N>;
46
47 explicit IndexRangesPartitioner(const R& ranges);
48
49 size_t size() const;
50
51 size_t active_rank() const;
52
53 // N == 1: no stride walk, no recursion -- direct unravel(), mirroring
54 // IndexRanges<T,1>::unravel() exactly (same as what the partitioners'
55 // rank==1 branches already do today). Caller (the scheduler) guarantees
56 // 0 <= flat_beg < flat_end <= size() -- no bounds checking performed here.
57 template <typename F>
58 bool for_each_box(size_t flat_beg, size_t flat_end, F&& visit) requires (N == 1);
59
60 // N > 1: general recursive box decomposition. Visits every hyperplane-
61 // aligned box covering [flat_beg, flat_end) of the cached ranges' active
62 // dims. Caller (the scheduler) guarantees 0 <= flat_beg < flat_end <=
63 // size() -- no bounds checking performed here (the recursion's own
64 // if (b >= e) { return false; } would already have absorbed an
65 // out-of-contract flat_beg >= flat_end regardless, so this was never
66 // load-bearing for N > 1 -- only N == 1 needed it removed explicitly).
67 template <typename F>
68 bool for_each_box(size_t flat_beg, size_t flat_end, F&& visit) requires (N > 1);
69
70 private:
71
72 void _set_point(size_t dim, size_t coord);
73 void _set_span(size_t dim, size_t b, size_t e);
74 void _emit_middle(size_t dim, size_t b, size_t e);
75
76 const R& _ranges;
77 size_t _active_rank;
78 std::array<size_t, N> _strides{};
79 R _box;
80};
81
82// Constructor
83template <typename T, size_t N>
84IndexRangesPartitioner<T, N>::IndexRangesPartitioner(const R& ranges) : _ranges(ranges) {
85
86 _active_rank = N;
87 std::array<size_t, N> extent{};
88 for (size_t d = 0; d < N; ++d) {
89 extent[d] = _ranges.size(d);
90 if (extent[d] == 0) {
91 _active_rank = d;
92 break;
93 }
94 }
95 if (_active_rank > 0) {
96 _strides[_active_rank - 1] = 1;
97 for (size_t d = _active_rank - 1; d-- > 0; ) {
98 _strides[d] = _strides[d + 1] * extent[d + 1];
99 }
100 }
101 for (size_t d = _active_rank; d < N; ++d) {
102 _box.dim(d) = _ranges.dim(d);
103 }
104}
105
106// Function: size
107template <typename T, size_t N>
108size_t IndexRangesPartitioner<T, N>::size() const {
109 return _ranges.size();
110}
111
112// Function: active_rank
113template <typename T, size_t N>
114size_t IndexRangesPartitioner<T, N>::active_rank() const {
115 return _active_rank;
116}
117
118// Function: for_each_box
119template <typename T, size_t N>
120template <typename F>
121bool IndexRangesPartitioner<T, N>::for_each_box(size_t flat_beg, size_t flat_end, F&& visit)
122requires (N == 1) {
123 if (_active_rank == 0) {
124 return false;
125 }
126 _box = _ranges.unravel(flat_beg, flat_end);
127 if constexpr (std::is_same_v<std::invoke_result_t<F, R>, bool>) {
128 return visit(_box);
129 } else {
130 visit(_box);
131 return false;
132 }
133}
134
135// Function: for_each_box
136template <typename T, size_t N>
137template <typename F>
138bool IndexRangesPartitioner<T, N>::for_each_box(size_t flat_beg, size_t flat_end, F&& visit)
139requires (N > 1) {
140
141 if (_active_rank == 0) {
142 return false;
143 }
144
145 // Runtime recursion over active dims only -- runs once per claim, not
146 // per element, so this is O(active_rank) stack frames with O(1) work
147 // each (thanks to the precomputed _strides table and the incremental
148 // _box updates in _set_point/_set_span/_emit_middle), never O(chunk_size).
149 auto recurse = [&](auto& self, size_t dim, size_t b, size_t e) -> bool {
150 if (b >= e) {
151 return false;
152 }
153
154 if (dim == _active_rank - 1) {
155 _set_span(dim, b, e);
156 if constexpr (std::is_same_v<std::invoke_result_t<F, R>, bool>) {
157 return visit(_box);
158 } else {
159 visit(_box);
160 return false;
161 }
162 }
163
164 size_t s = _strides[dim];
165 size_t outer_b = b / s, inner_b = b % s;
166 size_t outer_e = e / s, inner_e = e % s;
167
168 if (outer_b == outer_e) {
169 _set_point(dim, outer_b);
170 return self(self, dim + 1, inner_b, inner_e);
171 }
172 if (inner_b != 0) {
173 _set_point(dim, outer_b);
174 if (self(self, dim + 1, inner_b, s)) {
175 return true;
176 }
177 ++outer_b;
178 }
179 if (outer_b < outer_e) {
180 _emit_middle(dim, outer_b, outer_e);
181 if constexpr (std::is_same_v<std::invoke_result_t<F, R>, bool>) {
182 if (visit(_box)) {
183 return true;
184 }
185 } else {
186 visit(_box);
187 }
188 }
189 if (inner_e != 0) {
190 _set_point(dim, outer_e);
191 if (self(self, dim + 1, 0, inner_e)) {
192 return true;
193 }
194 }
195 return false;
196 };
197
198 return recurse(recurse, 0, flat_beg, flat_end);
199}
200
201// Function: _set_point
202template <typename T, size_t N>
203void IndexRangesPartitioner<T, N>::_set_point(size_t dim, size_t coord) {
204 auto [bd, ed, sd] = _ranges.dim(dim);
205 _box.dim(dim) = { static_cast<T>(bd + coord * sd), static_cast<T>(bd + (coord + 1) * sd), sd };
206}
207
208// Function: _set_span
209template <typename T, size_t N>
210void IndexRangesPartitioner<T, N>::_set_span(size_t dim, size_t b, size_t e) {
211 auto [bd, ed, sd] = _ranges.dim(dim);
212 _box.dim(dim) = { static_cast<T>(bd + b * sd), static_cast<T>(bd + e * sd), sd };
213}
214
215// Function: _emit_middle
216template <typename T, size_t N>
217void IndexRangesPartitioner<T, N>::_emit_middle(size_t dim, size_t b, size_t e) {
218 _set_span(dim, b, e);
219 for (size_t d = dim + 1; d < _active_rank; ++d) {
220 _box.dim(d) = _ranges.dim(d);
221 }
222}
223
224// ----------------------------------------------------------------------------
225// Partitioner Base
226// ----------------------------------------------------------------------------
227
293template <typename C = DefaultClosureWrapper>
295
296 public:
297
301 constexpr static bool is_default_wrapper_v = std::is_same_v<C, DefaultClosureWrapper>;
302
307
311 PartitionerBase() = default;
312
316 explicit PartitionerBase(size_t chunk_size);
317
322
326 size_t chunk_size() const;
327
331 void chunk_size(size_t cz);
332
336 const C& closure_wrapper() const;
337
342
346 template <typename F>
347 void closure_wrapper(F&& fn);
348
352 template <typename F>
353 decltype(auto) operator () (F&& callable);
354
355 protected:
356
360 size_t _chunk_size{0};
361
365 C _closure_wrapper;
366};
367
368// Constructor
369template <typename C>
372
373// Constructor
374template <typename C>
376 _chunk_size {chunk_size},
377 _closure_wrapper {std::forward<C>(closure_wrapper)} {
378}
379
380// Function: chunk_size
381template <typename C>
383 return _chunk_size;
384}
385
386// Function: chunk_size
387template <typename C>
389 _chunk_size = cz;
390}
391
392// Function: closure_wrapper
393template <typename C>
395 return _closure_wrapper;
396}
397
398// Function: closure_wrapper
399template <typename C>
401 return _closure_wrapper;
402}
403
404// Function: closure_wrapper
405template <typename C>
406template <typename F>
408 _closure_wrapper = std::forward<F>(fn);
409}
410
411// Operator: ()
412template <typename C>
413template <typename F>
414decltype(auto) PartitionerBase<C>::operator () (F&& callable) {
415 if constexpr(is_default_wrapper_v) {
416 return std::forward<F>(callable);
417 }
418 else {
419 // closure wrapper is stateful - capture it by reference
420 return [this, c=std::forward<F>(callable)]() mutable { _closure_wrapper(c); };
421 }
422}
423
424// ----------------------------------------------------------------------------
425// Static Partitioner
426// ----------------------------------------------------------------------------
427
475template <typename C = DefaultClosureWrapper>
477
478 public:
479
483 static constexpr PartitionerType type() { return PartitionerType::STATIC; }
484
488 StaticPartitioner() = default;
489
493 explicit StaticPartitioner(size_t sz);
494
498 explicit StaticPartitioner(size_t sz, C&& closure);
499
507 size_t adjusted_chunk_size(size_t N, size_t W, size_t w) const;
508
509 // --------------------------------------------------------------------------
510 // scheduling methods
511 // --------------------------------------------------------------------------
512
516 template <typename F>
517 void loop(size_t N, size_t W, size_t curr_b, size_t chunk_size, F&& func);
518
535 template <IndexRangesLike R, typename F>
536 void loop(const R& range, size_t N, size_t W, size_t curr_b, size_t chunk_size, F&& func) const;
537
538};
539
540// Constructor
541template <typename C>
544
545// Constructor
546template <typename C>
548 PartitionerBase<C>(sz, std::forward<C>(closure)) {
549}
550
551// Function: adjusted_chunk_size
552template <typename C>
553size_t StaticPartitioner<C>::adjusted_chunk_size(size_t N, size_t W, size_t w) const {
554 return this->_chunk_size ? this->_chunk_size : N/W + (w < N%W);
555}
556
557// Function: loop
558template <typename C>
559template <typename F>
560void StaticPartitioner<C>::loop(size_t N, size_t W, size_t curr_b, size_t chunk_size, F&& func) {
561 size_t stride = W * chunk_size;
562 while(curr_b < N) {
563 size_t curr_e = (std::min)(curr_b + chunk_size, N);
564 if constexpr (std::is_same_v<std::invoke_result_t<F, size_t, size_t>, bool>) {
565 if(func(curr_b, curr_e)) {
566 return;
567 }
568 } else {
569 func(curr_b, curr_e);
570 }
571 curr_b += stride;
572 }
573}
574
575// Function: loop
576template <typename C>
577template <IndexRangesLike R, typename F>
578void StaticPartitioner<C>::loop(
579 const R& range, size_t N, size_t W, size_t curr_b, size_t chunk_size, F&& func
580) const {
581 IndexRangesPartitioner irp(range);
582 size_t stride = W * chunk_size;
583 while(curr_b < N) {
584 size_t curr_e = (std::min)(curr_b + chunk_size, N);
585 if(irp.for_each_box(curr_b, curr_e, func)) {
586 return;
587 }
588 curr_b += stride;
589 }
590}
591
592// ----------------------------------------------------------------------------
593// Guided Partitioner
594// ----------------------------------------------------------------------------
595
635template <typename C = DefaultClosureWrapper>
637
638 public:
639
643 static constexpr PartitionerType type() { return PartitionerType::DYNAMIC; }
644
648 GuidedPartitioner() = default;
649
654 explicit GuidedPartitioner(size_t sz);
655
659 explicit GuidedPartitioner(size_t sz, C&& closure);
660
661 // --------------------------------------------------------------------------
662 // scheduling methods
663 // --------------------------------------------------------------------------
664
668 template <typename F>
669 void loop(size_t N, size_t W, std::atomic<size_t>& next, F&& func) const;
670
674 template <IndexRangesLike R, typename F>
675 void loop(const R& range, size_t N, size_t W, std::atomic<size_t>& next, F&& func) const;
676
677};
678
679// Constructor
680template <typename C>
683
684// Constructor
685template <typename C>
687 PartitionerBase<C>(sz, std::forward<C>(closure)) {
688}
689
690// Function: loop
691template <typename C>
692template <typename F>
693void GuidedPartitioner<C>::loop(
694 size_t N, size_t W, std::atomic<size_t>& next, F&& func
695) const {
696
697 size_t chunk_size = (this->_chunk_size == 0) ? size_t{1} : this->_chunk_size;
698 size_t p1 = 2 * W * (chunk_size + 1);
699 float p2 = 0.5f / static_cast<float>(W);
700 size_t curr_b = next.load(std::memory_order_relaxed);
701
702 while(curr_b < N) {
703 size_t r = N - curr_b;
704 size_t csize = (r < p1) ? chunk_size : (std::max)(static_cast<size_t>(p2 * r), chunk_size);
705 size_t curr_e = (std::min)(curr_b + csize, N);
706 if(next.compare_exchange_weak(curr_b, curr_e,
707 std::memory_order_relaxed,
708 std::memory_order_relaxed)) {
709 if constexpr (std::is_same_v<std::invoke_result_t<F, size_t, size_t>, bool>) {
710 if(func(curr_b, curr_e)) {
711 return;
712 }
713 } else {
714 func(curr_b, curr_e);
715 }
716 curr_b = curr_e;
717 }
718 }
719}
720
721// Function: loop
722template <typename C>
723template <IndexRangesLike R, typename F>
724void GuidedPartitioner<C>::loop(
725 const R& range, size_t N, size_t W, std::atomic<size_t>& next, F&& func
726) const {
727
728 IndexRangesPartitioner irp(range);
729
730 size_t chunk_size = (this->_chunk_size == 0) ? size_t{1} : this->_chunk_size;
731 size_t p1 = 2 * W * (chunk_size + 1);
732 float p2 = 0.5f / static_cast<float>(W);
733 size_t curr_b = next.load(std::memory_order_relaxed);
734
735 while(curr_b < N) {
736 size_t r = N - curr_b;
737 size_t csize = (r < p1) ? chunk_size : (std::max)(static_cast<size_t>(p2 * r), chunk_size);
738 size_t curr_e = (std::min)(curr_b + csize, N);
739 if(next.compare_exchange_weak(curr_b, curr_e,
740 std::memory_order_relaxed,
741 std::memory_order_relaxed)) {
742 if(irp.for_each_box(curr_b, curr_e, func)) {
743 return;
744 }
745 curr_b = curr_e;
746 }
747 }
748}
749
750// ----------------------------------------------------------------------------
751// Dynamic Partitioner
752// ----------------------------------------------------------------------------
753
793template <typename C = DefaultClosureWrapper>
795
796 public:
797
801 static constexpr PartitionerType type() { return PartitionerType::DYNAMIC; }
802
807
811 explicit DynamicPartitioner(size_t sz);
812
816 explicit DynamicPartitioner(size_t sz, C&& closure);
817
818 // --------------------------------------------------------------------------
819 // scheduling methods
820 // --------------------------------------------------------------------------
821
825 template <typename F>
826 void loop(size_t N, size_t, std::atomic<size_t>& next, F&& func) const;
827
831 template <IndexRangesLike R, typename F>
832 void loop(const R& range, size_t N, size_t, std::atomic<size_t>& next, F&& func) const;
833
834};
835
836// Constructor
837template <typename C>
840
841// Constructor
842template <typename C>
844 PartitionerBase<C>(sz, std::forward<C>(closure)) {
845}
846
847// Function: loop
848template <typename C>
849template <typename F>
850void DynamicPartitioner<C>::loop(size_t N, size_t, std::atomic<size_t>& next, F&& func) const {
851
852 size_t chunk_size = (this->_chunk_size == 0) ? size_t{1} : this->_chunk_size;
853 size_t curr_b = next.fetch_add(chunk_size, std::memory_order_relaxed);
854
855 while(curr_b < N) {
856 if constexpr (std::is_same_v<std::invoke_result_t<F, size_t, size_t>, bool>) {
857 if(func(curr_b, (std::min)(curr_b + chunk_size, N))) {
858 return;
859 }
860 } else {
861 func(curr_b, (std::min)(curr_b + chunk_size, N));
862 }
863 curr_b = next.fetch_add(chunk_size, std::memory_order_relaxed);
864 }
865}
866
867// Function: loop
868template <typename C>
869template <IndexRangesLike R, typename F>
870void DynamicPartitioner<C>::loop(
871 const R& range, size_t N, size_t, std::atomic<size_t>& next, F&& func
872) const {
873
874 IndexRangesPartitioner irp(range);
875
876 size_t curr_b = next.load(std::memory_order_relaxed);
877 size_t chunk_size = (this->_chunk_size == 0) ? size_t{1} : this->_chunk_size;
878
879 while(curr_b < N) {
880
881 size_t curr_e = (std::min)(curr_b + chunk_size, N);
882 if(next.compare_exchange_weak(curr_b, curr_e,
883 std::memory_order_relaxed,
884 std::memory_order_relaxed)) {
885 if(irp.for_each_box(curr_b, curr_e, func)) {
886 return;
887 }
888 curr_b = curr_e;
889 }
890 }
891}
892
893// ----------------------------------------------------------------------------
894// RandomPartitioner
895// ----------------------------------------------------------------------------
896
936template <typename C = DefaultClosureWrapper>
938
939 public:
940
944 static constexpr PartitionerType type() { return PartitionerType::DYNAMIC; }
945
949 RandomPartitioner() = default;
950
954 explicit RandomPartitioner(size_t sz);
955
959 explicit RandomPartitioner(size_t sz, C&& closure);
960
964 RandomPartitioner(float alpha, float beta);
965
969 RandomPartitioner(float alpha, float beta, C&& closure);
970
974 float alpha() const;
975
979 float beta() const;
980
987 std::pair<size_t, size_t> chunk_size_range(size_t N, size_t W) const;
988
989 // --------------------------------------------------------------------------
990 // scheduling methods
991 // --------------------------------------------------------------------------
992
996 template <typename F>
997 void loop(size_t N, size_t W, std::atomic<size_t>& next, F&& func) const;
998
1002 template <IndexRangesLike R, typename F>
1003 void loop(const R& range, size_t N, size_t W, std::atomic<size_t>& next, F&& func) const;
1004
1005 private:
1006
1007 float _alpha {0.01f};
1008 float _beta {0.50f};
1009};
1010
1011// Constructor
1012template <typename C>
1015
1016// Constructor
1017template <typename C>
1019 PartitionerBase<C>(sz, std::forward<C>(closure)) {
1020}
1021
1022// Constructor
1023template <typename C>
1025}
1026
1027// Constructor
1028template <typename C>
1030 _alpha {alpha}, _beta {beta},
1031 PartitionerBase<C>(0, std::forward<C>(closure)) {
1032}
1033
1034// Function: alpha
1035template <typename C>
1037 return _alpha;
1038}
1039
1040// Function: beta
1041template <typename C>
1043 return _beta;
1044}
1045
1046// Function: chunk_size_range
1047template <typename C>
1048std::pair<size_t, size_t> RandomPartitioner<C>::chunk_size_range(size_t N, size_t W) const {
1049
1050 size_t b1 = static_cast<size_t>(_alpha * N * W);
1051 size_t b2 = static_cast<size_t>(_beta * N * W);
1052
1053 if(b1 > b2) {
1054 std::swap(b1, b2);
1055 }
1056
1057 b1 = (std::max)(b1, size_t{1});
1058 b2 = (std::max)(b2, b1 + 1);
1059
1060 return {b1, b2};
1061}
1062
1063// Function: loop
1064template <typename C>
1065template <typename F>
1066void RandomPartitioner<C>::loop(
1067 size_t N, size_t W, std::atomic<size_t>& next, F&& func
1068) const {
1069
1070 auto [b1, b2] = chunk_size_range(N, W);
1071
1072 std::default_random_engine engine {std::random_device{}()};
1073 std::uniform_int_distribution<size_t> dist(b1, b2);
1074
1075 size_t chunk_size = dist(engine);
1076 size_t curr_b = next.fetch_add(chunk_size, std::memory_order_relaxed);
1077
1078 while(curr_b < N) {
1079 if constexpr (std::is_same_v<std::invoke_result_t<F, size_t, size_t>, bool>) {
1080 if(func(curr_b, (std::min)(curr_b + chunk_size, N))) {
1081 return;
1082 }
1083 } else {
1084 func(curr_b, (std::min)(curr_b + chunk_size, N));
1085 }
1086 chunk_size = dist(engine);
1087 curr_b = next.fetch_add(chunk_size, std::memory_order_relaxed);
1088 }
1089}
1090
1091// Function: loop
1092template <typename C>
1093template <IndexRangesLike R, typename F>
1094void RandomPartitioner<C>::loop(
1095 const R& range, size_t N, size_t W, std::atomic<size_t>& next, F&& func
1096) const {
1097
1098 IndexRangesPartitioner irp(range);
1099
1100 auto [b1, b2] = chunk_size_range(N, W);
1101
1102 std::default_random_engine engine{std::random_device{}()};
1103 std::uniform_int_distribution<size_t> dist(b1, b2);
1104
1105 size_t curr_b = next.load(std::memory_order_relaxed);
1106
1107 while(curr_b < N) {
1108 size_t curr_e = (std::min)(curr_b + dist(engine), N);
1109 if(next.compare_exchange_weak(curr_b, curr_e,
1110 std::memory_order_relaxed,
1111 std::memory_order_relaxed)) {
1112 if(irp.for_each_box(curr_b, curr_e, func)) {
1113 return;
1114 }
1115 curr_b = curr_e;
1116 }
1117 }
1118}
1119
1120// ------------------------------------------------------------------------------------------------
1121// Concept and Alias
1122// ------------------------------------------------------------------------------------------------
1123
1131
1137template <typename P>
1138concept PartitionerLike = std::derived_from<P, PartitionerBase<typename P::closure_wrapper_type>>;
1139
1147template <typename P>
1148inline constexpr bool is_partitioner_v = PartitionerLike<P>;
1149
1150} // end of namespace tf -------------------------------------------------------------------------
class to create a default closure wrapper
Definition partitioner.hpp:31
DynamicPartitioner()=default
default constructor
static constexpr PartitionerType type()
queries the partition type (dynamic)
Definition partitioner.hpp:801
class to create a guided partitioner for scheduling parallel algorithms
Definition partitioner.hpp:636
GuidedPartitioner()=default
default constructor
static constexpr PartitionerType type()
queries the partition type (dynamic)
Definition partitioner.hpp:643
class to create an N-dimensional index range of integral indices
Definition iterator.hpp:188
PartitionerBase(size_t chunk_size)
construct a partitioner with the given chunk size
Definition partitioner.hpp:370
static constexpr bool is_default_wrapper_v
indicating if the given closure wrapper is a default wrapper (i.e., empty)
Definition partitioner.hpp:301
C closure_wrapper_type
the closure type
Definition partitioner.hpp:306
void chunk_size(size_t cz)
update the chunk size of this partitioner
Definition partitioner.hpp:388
const DefaultClosureWrapper & closure_wrapper() const
Definition partitioner.hpp:394
void closure_wrapper(F &&fn)
modify the closure wrapper object
Definition partitioner.hpp:407
PartitionerBase(size_t chunk_size, C &&closure_wrapper)
construct a partitioner with the given chunk size and closure wrapper
Definition partitioner.hpp:375
C & closure_wrapper()
acquire a mutable access to the closure wrapper object
Definition partitioner.hpp:400
PartitionerBase()=default
default constructor
size_t chunk_size() const
Definition partitioner.hpp:382
std::pair< size_t, size_t > chunk_size_range(size_t N, size_t W) const
queries the range of chunk size
Definition partitioner.hpp:1048
RandomPartitioner()=default
default constructor
static constexpr PartitionerType type()
queries the partition type (dynamic)
Definition partitioner.hpp:944
float alpha() const
queries the alpha value
Definition partitioner.hpp:1036
float beta() const
queries the beta value
Definition partitioner.hpp:1042
StaticPartitioner()=default
default constructor
size_t adjusted_chunk_size(size_t N, size_t W, size_t w) const
queries the adjusted chunk size
Definition partitioner.hpp:553
static constexpr PartitionerType type()
queries the partition type (static)
Definition partitioner.hpp:483
concept to check if a type is a partitioner
Definition partitioner.hpp:1138
taskflow namespace
Definition small_vector.hpp:20
@ STATIC
static task type
Definition task.hpp:25
PartitionerType
enumeration of all partitioner types
Definition partitioner.hpp:19
@ DYNAMIC
dynamic partitioner type
Definition partitioner.hpp:23
@ STATIC
static partitioner type
Definition partitioner.hpp:21
constexpr bool is_partitioner_v
concept to check if a type is a partitioner (variable template)
Definition partitioner.hpp:1148
GuidedPartitioner<> DefaultPartitioner
default partitioner set to tf::GuidedPartitioner
Definition partitioner.hpp:1130