dispenso 1.6.2
A library for task parallelism
Loading...
Searching...
No Matches
graph.h
Go to the documentation of this file.
1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
14#pragma once
15
16#include <atomic>
17#include <deque>
18#include <limits>
19#include <memory>
20#include <type_traits>
21#include <vector>
22
24#include <dispenso/platform.h>
28
38#ifndef DISPENSO_GRAPH_DEPENDENTS_INLINE_SIZE
39#define DISPENSO_GRAPH_DEPENDENTS_INLINE_SIZE 4
40#endif
41
42static_assert(
43 DISPENSO_GRAPH_DEPENDENTS_INLINE_SIZE >= 1 && DISPENSO_GRAPH_DEPENDENTS_INLINE_SIZE <= 64,
44 "DISPENSO_GRAPH_DEPENDENTS_INLINE_SIZE must be between 1 and 64");
45
46/*
47Terminology
48--------------------------------------------------------------------------------
49The Node depends on the predecessor. The dependent depends on the node.
50~~~
51┌─────────────┐ ┌──────┐ ┌───────────┐
52│ predecessor │ ──▶ │ node │ ──▶ │ dependent │
53└─────────────┘ └──────┘ └───────────┘
54~~~
55
56Graph construction
57--------------------------------------------------------------------------------
58The Graph class can be used to created tasks with dependencies and execute it once.
59Graphs must not contain cycles.
60Example:
61~~~
62//
63// ┌────────────┐ ┌───────────────┐ ┌───────────────────┐
64// │ 0: r[0]+=1 │ ──▶ │ 1: r[1]=r[0]*2│ ──▶ │ 4: r[4]=r[1]+r[3] │
65// └────────────┘ └───────────────┘ └───────────────────┘
66// ▲
67// ┌────────────┐ ┌───────────────┐ │
68// │ 2: r[2]+=8 │ ──▶ │ 3: r[3]=r[2]/2│ ──────┘
69// └────────────┘ └───────────────┘
70
71std::array<float, 5> r;
72
73dispenso::Graph graph;
74
75dispenso::Node& N0 = graph.addNode([&]() { r[0] += 1; });
76dispenso::Node& N2 = graph.addNode([&]() { r[2] += 8; });
77dispenso::Node& N1 = graph.addNode([&]() { r[1] += r[0] * 2; });
78dispenso::Node& N3 = graph.addNode([&]() { r[3] += r[2] / 2; });
79dispenso::Node& N4 = graph.addNode([&]() { r[4] += r[1] + r[3]; });
80
81N4.dependsOn(N1, N3);
82N1.dependsOn(N0);
83N3.dependsOn(N2);
84
85dispenso::TaskSet taskSet(dispenso::globalThreadPool());
86dispenso::ParallelForExecutor parallelForExecutor;
87parallelForExecutor(taskSet, graph);
88~~~
89
90Partial revaluation
91--------------------------------------------------------------------------------
92If graph is big or we need to recompute graph partially we can execute it again.
93After execution of the graph all nodes change their state from "incomplete" to
94"completed". If order to evaluate whole graph again we can use function `setAllNodesIncomplete`
95Example:
96~~~
97r = {0, 0, 0, 0, 0};
98setAllNodesIncomplete(graph);
99parallelForExecutor(taskSet, graph);
100~~~
101
102The graph can be recomputed partially if we have new input data for one or several nodes in the
103graph. It order to do it we need to call `setIncomplete()` method for every node which we need to
104recompute and after use functor `ForwardPropagator` to mark as "incomplete" all dependents.
105
106Example:
107~~~
108N1.setIncomplete();
109r[1] = r[4] = 0;
110ForwardPropagator forwardPropagator;
111forwardPropagator(graph);
112evaluateGraph(graph);
113~~~
114In this exaple only node 1 and 4 will be invoked.
115
116 Subgraphs
117--------------------------------------------------------------------------------
118It is possible to organize nodes into subgraphs and destroy and recreate if we have static and
119dynamic parts of the computation graph
120Example:
121~~~
122//
123// ∙----subgraph1---∙ ∙---subgraph2-------∙
124// ¦ ┌────────────┐ ¦ ¦ ┌───────────────┐ ¦ ┌───────────────────┐
125// ¦ │ 0: r[0]+=1 │ ──▶ │ 1: r[1]=r[0]*2│ ──▶ │ 4: r[4]=r[1]+r[3] │
126// ¦ └────────────┘ ¦ ¦ └───────────────┘ ¦ └───────────────────┘
127// ¦ ¦ ¦ ¦ ▲
128// ¦ ┌────────────┐ ¦ ¦ ┌───────────────┐ ¦ │
129// ¦ │ 2: r[2]+=8 │ ──▶ │ 3: r[3]=r[2]/2│ ──────┘
130// ¦ └────────────┘ ¦ ¦ └───────────────┘ ¦
131// ∙----------------∙ ∙-------------------∙
132std::array<float, 5> r;
133dispenso::Graph graph;
134
135dispenso::Subgraph& subgraph1 = graph.addSubgraph();
136dispenso::Subgraph& subgraph2 = graph.addSubgraph();
137
138dispenso::Node& N0 = subgraph1.addNode([&]() { r[0] += 1; });
139dispenso::Node& N2 = subgraph1.addNode([&]() { r[2] += 8; });
140dispenso::Node& N1 = subgraph2.addNode([&]() { r[1] += r[0] * 2; });
141dispenso::Node& N3 = subgraph2.addNode([&]() { r[3] += r[2] / 2; });
142dispenso::Node& N4 = graph.addNode([&]() { r[4] += r[1] + r[3]; });
143
144N4.dependsOn(N1, N3);
145N1.dependsOn(N0);
146N3.dependsOn(N2);
147
148// evaluate graph first time
149r = {0, 0, 0, 0, 0};
150dispenso::ConcurrentTaskSet concurrentTaskSet(dispenso::globalThreadPool());
151dispenso::ConcurrentTaskSetExecutor concurrentTaskSetExecutor;
152concurrentTaskSetExecutor(concurrentTaskSet, graph);
153
154// disconnect and destroy nodes of subgraph2
155// it invalidates node references/pointers of this subgraph
156subgraph2.clear();
157
158// create another nodes
159dispenso::Node& newN1 = subgraph2.addNode([&]() { r[1] += r[0] * 20; });
160dispenso::Node& newN3 = subgraph2.addNode([&]() { r[3] += r[2] / 20; });
161newN1.dependsOn(N0);
162newN3.dependsOn(N2);
163N4.dependsOn(newN1, newN3);
164
165// and revaluae the graph
166setAllNodesIncomplete(movedGraph);
167concurrentTaskSetExecutor(concurrentTaskSet, graph);
168~~~
169
170Bidirectional propagation dependency
171--------------------------------------------------------------------------------
172In certain scenarios, nodes may alter the same memory. In such instances, it becomes necessary to
173compute the predecessors of the node, even if they possess a "completed" state following state
174propagation. To facilitate this process automatically, we introduce the notion of a bidirectional
175propagation dependency (`BiProp`).
176
177Example:
178~~~
179// ┌─────────────────┐
180// │ 3: m3+=b*b │
181// └─────────────────┘
182// ▲
183// ┌−-----------−−−−−−−−−│−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−┐
184// ╎ ┌───────────┐ ┌─────────────────┐ ┌────────────┐ ╎
185// ╎ │ 0: b+=5 │ ──▷ │ 1: b*=5 │ ──▷ │ 2: b/=m4 │ ╎
186// ╎ └───────────┘ └─────────────────┘ └────────────┘ ╎
187// └−−−-−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−▲−−−−−−−−−−−−┘
188// Legend: │
189// ──▶ Normal dependency ┌────────────┐
190// ──▷ Bidirectional propagation dependency │ 4: m4+=2 │
191// m4 variable modified only in node 4 └────────────┘
192
193float b, m3, m4
194dispenso::BiPropGraph g;
195std::array<dispenso::BiPropNode*, 8> N;
196
197dispenso::BiPropNode& N0 = g.addNode([&]() { b += 5; });
198dispenso::BiPropNode& N1 = g.addNode([&]() { b *= 5; });
199dispenso::BiPropNode& N2 = g.addNode([&]() { b /= m4; });
200dispenso::BiPropNode& N3 = g.addNode([&]() { m3 += b*b; });
201dispenso::BiPropNode& N4 = g.addNode([&]() { m4 += 2; });
202
203N3.dependsOn(N1);
204N2.dependsOn(N4);
205N2.biPropDependsOn(N1);
206N1.biPropDependsOn(N0);
207
208// first execution
209b = m3 = m4 = 0.f;
210dispenso::ConcurrentTaskSet concurrentTaskSet(dispenso::globalThreadPool());
211dispenso::ConcurrentTaskSetExecutor concurrentTaskSetExecutor;
212concurrentTaskSetExecutor(concurrentTaskSet, g);
213
214N[4].setIncomplete();
215// if node 4 is incomplete after propagation node 2 become incomplete. Taking in account that node 2
216// bidirectionally depends on nodes 0 and 1 they will be marked as incomplete as well
217b = m4 = 0.f;
218ForwardPropagator forwardPropagator;
219forwardPropagator(g);
220concurrentTaskSetExecutor(concurrentTaskSet, g);
221~~~
222
223Please read tests from `graph_test.cpp` for more examples.
224*/
225
226namespace dispenso {
227
228namespace detail {
229class ExecutorBase;
230
231template <typename F>
232void callFunctor(void* ptr) {
233 (*static_cast<F*>(ptr))();
234}
235
236template <typename F>
237void destroyFunctor(void* ptr) {
238 static_cast<F*>(ptr)->~F();
239 constexpr size_t kFuncSize = static_cast<size_t>(dispenso::detail::nextPow2(sizeof(F)));
240 dispenso::deallocSmallBuffer<kFuncSize>(ptr);
241}
242} // namespace detail
243
247class Node {
248 public:
249 Node() = delete;
250 Node(const Node&) = delete;
251 Node& operator=(const Node&) = delete;
253 Node(Node&& other) noexcept
254 : numIncompletePredecessors_(other.numIncompletePredecessors_.load()),
255 numPredecessors_(other.numPredecessors_),
256 invoke_(other.invoke_),
257 destroy_(other.destroy_),
258 funcBuffer_(other.funcBuffer_),
259 dependents_(std::move(other.dependents_)) {
260 other.funcBuffer_ = nullptr;
261 }
262 ~Node() {
263 if (funcBuffer_) {
264 destroy_(funcBuffer_);
265 }
266 }
272 template <typename... Ns>
273 inline void dependsOn(Ns&... nodes) {
274 ((void)std::initializer_list<int>{(dependsOnOneNode(nodes), 0)...});
275 }
280 inline void run() const {
281 invoke_(funcBuffer_);
282 numIncompletePredecessors_.store(kCompleted, std::memory_order_release);
283 }
289 template <class F>
290 inline void forEachDependent(F&& func) const {
291 for (const Node* dependent : dependents_) {
292 func(*dependent);
293 }
294 }
300 template <class F>
301 inline void forEachDependent(F&& func) {
302 for (Node* dependent : dependents_) {
303 func(*dependent);
304 }
305 }
309 inline size_t numPredecessors() const {
310 return numPredecessors_;
311 }
318 inline bool isCompleted() const {
319 return numIncompletePredecessors_.load(std::memory_order_relaxed) == kCompleted;
320 }
328 inline bool setIncomplete() const {
329 if (numIncompletePredecessors_.load(std::memory_order_relaxed) == kCompleted) {
330 numIncompletePredecessors_.store(0, std::memory_order_relaxed);
331 return true;
332 }
333 return false;
334 }
335
341 inline void setCompleted() const {
342 numIncompletePredecessors_.store(kCompleted, std::memory_order_relaxed);
343 }
344
345 protected:
347#if DISPENSO_HAS_CONCEPTS
348 template <class F>
349 requires(!std::derived_from<std::remove_cvref_t<F>, Node>)
350 Node(F&& f) : numIncompletePredecessors_(0) {
351#else
352 template <class F, class X = std::enable_if_t<!std::is_base_of<Node, F>::value, void>>
353 Node(F&& f) : numIncompletePredecessors_(0) {
354#endif
355 using FNoRef = typename std::remove_reference<F>::type;
356
357 constexpr size_t kFuncSize = static_cast<size_t>(detail::nextPow2(sizeof(FNoRef)));
358 // ~Node does not run if this constructor throws, so return the buffer here if copying or
359 // moving the functor into it throws.
360 struct BufferGuard {
361 char* buf;
362 ~BufferGuard() {
363 if (buf) {
364 deallocSmallBuffer<kFuncSize>(buf);
365 }
366 }
367 } guard{allocSmallBuffer<kFuncSize>()};
368 new (guard.buf) FNoRef(std::forward<F>(f));
369 funcBuffer_ = guard.buf;
370 guard.buf = nullptr;
371 invoke_ = detail::callFunctor<FNoRef>;
372 destroy_ = detail::destroyFunctor<FNoRef>;
373 }
374
375 void dependsOnOneNode(Node& node) {
376 node.dependents_.emplace_back(this);
377 numPredecessors_++;
378 }
379
380 static constexpr size_t kCompleted = std::numeric_limits<size_t>::max();
381 mutable std::atomic<size_t> numIncompletePredecessors_;
382 size_t numPredecessors_ = 0;
383
384 private:
385 using InvokerType = void (*)(void* ptr);
386
387 InvokerType invoke_;
388 InvokerType destroy_;
389 char* funcBuffer_;
390
391 SmallVector<Node*, DISPENSO_GRAPH_DEPENDENTS_INLINE_SIZE> dependents_; // nodes depend on this
392
393 template <class N>
394 friend class SubgraphT;
395 friend class detail::ExecutorBase;
396 template <typename G>
397 friend void setAllNodesIncomplete(const G& graph);
398};
403class BiPropNode : public Node {
404 public:
405 BiPropNode() = delete;
406 BiPropNode(const BiPropNode&) = delete;
407 BiPropNode& operator=(const BiPropNode&) = delete;
409 BiPropNode(BiPropNode&& other) noexcept
410 : Node(std::move(other)), biPropSet_(std::move(other.biPropSet_)) {}
417 template <class... Ns>
418 inline void biPropDependsOn(Ns&... nodes) {
419 ((void)std::initializer_list<int>{(biPropDependsOnOneNode(nodes), 0)...});
420 }
427 inline bool isSameSet(const BiPropNode& node) const {
428 return biPropSet_ && biPropSet_ == node.biPropSet_;
429 }
430
431 private:
432#if DISPENSO_HAS_CONCEPTS
433 template <class T>
434 requires(!std::derived_from<std::remove_cvref_t<T>, BiPropNode>)
435 BiPropNode(T&& f) : Node(std::forward<T>(f)) {}
436#else
437 template <class T, class X = std::enable_if_t<!std::is_base_of<BiPropNode, T>::value, void>>
438 BiPropNode(T&& f) : Node(std::forward<T>(f)) {}
439#endif
440 inline void removeFromBiPropSet() {
441 if (biPropSet_ != nullptr) {
442 auto it = std::find(biPropSet_->begin(), biPropSet_->end(), this);
443 if (it != biPropSet_->end()) {
444 biPropSet_->erase(it);
445 }
446 }
447 }
448
449 DISPENSO_DLL_ACCESS void biPropDependsOnOneNode(BiPropNode& node);
450
451 std::shared_ptr<std::vector<const BiPropNode*>> biPropSet_;
452
453 template <class N>
454 friend class SubgraphT;
455 friend class detail::ExecutorBase;
456};
457
458template <class N>
459class GraphT;
460
466template <class N>
467class DISPENSO_DLL_ACCESS SubgraphT {
468 public:
469 using NodeType = N;
470 SubgraphT() = delete;
471 SubgraphT(const SubgraphT<N>&) = delete;
472 SubgraphT<N>& operator=(const SubgraphT<N>&) = delete;
474 SubgraphT(SubgraphT<N>&& other) noexcept
475 : graph_(other.graph_),
476 nodes_(std::move(other.nodes_)),
477 allocator_(std::move(other.allocator_)) {}
478 ~SubgraphT();
485 template <class T>
486 DISPENSO_REQUIRES(OnceCallableFunc<T>)
487 N& addNode(T&& f) {
488 nodes_.push_back(new (allocator_->alloc()) NodeType(std::forward<T>(f)));
489 return *nodes_.back();
490 }
494 size_t numNodes() const {
495 return nodes_.size();
496 }
505 void reserve(size_t n) {
506 nodes_.reserve(n);
507 }
513 const N& node(size_t index) const {
514 return *nodes_[index];
515 }
521 N& node(size_t index) {
522 return *nodes_[index];
523 }
529 template <class F>
530 inline void forEachNode(F&& func) const {
531 for (const N* node : nodes_) {
532 func(*node);
533 }
534 }
541 template <class F>
542 inline void forEachNode(F&& func) {
543 for (N* node : nodes_) {
544 func(*node);
545 }
546 }
551 void clear();
552
553 private:
554 using DeallocFunc = void (*)(NoLockPoolAllocator*);
555 using PoolPtr = std::unique_ptr<NoLockPoolAllocator, DeallocFunc>;
556
557 static constexpr size_t kNodeSizeP2 = static_cast<size_t>(detail::nextPow2(sizeof(NodeType)));
558
559 explicit SubgraphT(GraphT<N>* graph) : graph_(graph), nodes_(), allocator_(getAllocator()) {}
560
561 inline void removeNodeFromBiPropSet(Node* /* node */) {}
562 void removeNodeFromBiPropSet(BiPropNode* node) {
563 node->removeFromBiPropSet();
564 }
565 void decrementDependentCounters();
566 size_t markNodesWithPredicessors();
567 void removePredecessorDependencies(size_t numGraphPredecessors);
568
569 void destroyNodes();
570
571 static PoolPtr getAllocator();
572 static void releaseAllocator(NoLockPoolAllocator* ptr);
573
574 GraphT<N>* graph_;
575#if defined(_WIN32) && !defined(__MINGW32__)
576#pragma warning(push)
577#pragma warning(disable : 4251)
578#endif
579 std::vector<N*> nodes_;
580
581 PoolPtr allocator_;
582#if defined(_WIN32) && !defined(__MINGW32__)
583#pragma warning(pop)
584#endif
585
586 template <class T>
587 friend class GraphT;
588};
589
599template <class N>
600class DISPENSO_DLL_ACCESS GraphT {
601 public:
602 using NodeType = N;
604 GraphT(const GraphT<N>&) = delete;
605 GraphT& operator=(const GraphT<N>&) = delete;
610 subgraphs_.push_back(SubgraphType(this));
611 }
619 GraphT<N>& operator=(GraphT&& other) noexcept;
626 template <class T>
627 DISPENSO_REQUIRES(OnceCallableFunc<T>)
628 N& addNode(T&& f) {
629 return subgraphs_[0].addNode(std::forward<T>(f));
630 }
634 size_t numNodes() const {
635 return subgraphs_[0].numNodes();
636 }
640 void reserve(size_t n) {
641 subgraphs_[0].reserve(n);
642 }
648 const N& node(size_t index) const {
649 return subgraphs_[0].node(index);
650 }
656 N& node(size_t index) {
657 return subgraphs_[0].node(index);
658 }
666 size_t numSubgraphs() const {
667 return subgraphs_.size();
668 }
674 const SubgraphT<N>& subgraph(size_t index) const {
675 return subgraphs_[index];
676 }
682 SubgraphT<N>& subgraph(size_t index) {
683 return subgraphs_[index];
684 }
690 template <class F>
691 inline void forEachSubgraph(F&& func) const {
692 for (const SubgraphT<N>& subgraph : subgraphs_) {
693 func(subgraph);
694 }
695 }
701 template <class F>
702 inline void forEachSubgraph(F&& func) {
703 for (SubgraphT<N>& subgraph : subgraphs_) {
704 func(subgraph);
705 }
706 }
713 template <class F>
714 inline void forEachNode(F&& func) const {
715 for (const SubgraphT<N>& subgraph : subgraphs_) {
716 for (const N* node : subgraph.nodes_) {
717 func(*node);
718 }
719 }
720 }
726 template <class F>
727 inline void forEachNode(F&& func) {
728 for (SubgraphT<N>& subgraph : subgraphs_) {
729 for (N* node : subgraph.nodes_) {
730 func(*node);
731 }
732 }
733 }
737 inline void clear() {
738 subgraphs_.clear();
739 subgraphs_.push_back(SubgraphType(this));
740 }
744 inline void clearSubgraphs() {
745 for (SubgraphT<N>& subgraph : subgraphs_) {
746 subgraph.destroyNodes();
747 }
748 }
749
750 private:
751 static constexpr size_t kSubgraphSizeP2 =
752 static_cast<size_t>(detail::nextPow2(sizeof(SubgraphType)));
753
754#if defined(_WIN32) && !defined(__MINGW32__)
755#pragma warning(push)
756#pragma warning(disable : 4251)
757#endif
758 std::deque<SubgraphT<N>> subgraphs_;
759#if defined(_WIN32) && !defined(__MINGW32__)
760#pragma warning(pop)
761#endif
762
763 template <class T>
764 friend class SubgraphT;
765};
766
771
776} // namespace dispenso
BiPropNode(BiPropNode &&other) noexcept
Definition graph.h:409
bool isSameSet(const BiPropNode &node) const
Definition graph.h:427
void biPropDependsOn(Ns &... nodes)
Definition graph.h:418
void reserve(size_t n)
Definition graph.h:640
void forEachNode(F &&func) const
Definition graph.h:714
void clear()
Definition graph.h:737
void forEachNode(F &&func)
Definition graph.h:727
void forEachSubgraph(F &&func) const
Definition graph.h:691
size_t numSubgraphs() const
Definition graph.h:666
void forEachSubgraph(F &&func)
Definition graph.h:702
N & node(size_t index)
Definition graph.h:656
const SubgraphT< N > & subgraph(size_t index) const
Definition graph.h:674
GraphT(GraphT< N > &&other)
const N & node(size_t index) const
Definition graph.h:648
SubgraphT< N > & subgraph(size_t index)
Definition graph.h:682
void clearSubgraphs()
Definition graph.h:744
SubgraphT< N > & addSubgraph()
size_t numNodes() const
Definition graph.h:634
bool isCompleted() const
Definition graph.h:318
bool setIncomplete() const
Definition graph.h:328
Node(F &&f)
Definition graph.h:353
void setCompleted() const
Definition graph.h:341
void forEachDependent(F &&func) const
Definition graph.h:290
void forEachDependent(F &&func)
Definition graph.h:301
void dependsOn(Ns &... nodes)
Definition graph.h:273
size_t numPredecessors() const
Definition graph.h:309
Node(Node &&other) noexcept
Definition graph.h:253
void run() const
Definition graph.h:280
const N & node(size_t index) const
Definition graph.h:513
void forEachNode(F &&func) const
Definition graph.h:530
N & node(size_t index)
Definition graph.h:521
size_t numNodes() const
Definition graph.h:494
void reserve(size_t n)
Definition graph.h:505
void forEachNode(F &&func)
Definition graph.h:542
SubgraphT(SubgraphT< N > &&other) noexcept
Definition graph.h:474