GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 4 / 0 / 4
Functions: 100.0% 2 / 0 / 2
Branches: -% 0 / 0 / 0

include/brotensor/detail/cpu/thread_pool.h
Line Branch Exec Source
1 #pragma once
2
3 // Minimal lock-free thread pool for parallelizing embarrassingly-parallel
4 // CPU ops (matmul's row axis, a norm's batch*channel axis, attention's
5 // batch*head axis, ...). Per this project's convention (atomics / single-
6 // owner / queues, never a lock), work handoff uses std::atomic<T>::wait /
7 // notify (C++20) instead of a condition_variable + mutex, and work items
8 // are claimed via an atomic fetch_add cursor rather than a guarded queue.
9 //
10 // Usage: brotensor::detail::cpu::parallel_for(n, [&](std::size_t i) { ... });
11 // blocks the calling thread until fn(i) has run for every i in [0, n) —
12 // across the pool's workers plus the calling thread itself, which also
13 // drains work instead of idling. Falls back to a plain sequential loop
14 // when n <= 1 or the host has no spare cores, so tiny problem sizes don't
15 // pay thread hand-off cost.
16 //
17 // Single job in flight at a time: ThreadPool::instance() is a process-wide
18 // singleton, and run() assumes it is not re-entered from a second
19 // concurrent application thread while a call is outstanding (matches this
20 // library's usage — CPU ops are driven from one calling thread; run()
21 // parallelizes *within* one op call, not across concurrent callers).
22 //
23 // Nested calls (an outer parallel_for's callback invoking parallel_for
24 // again — e.g. one op calling another that also parallelizes internally)
25 // are safe but not further parallelized: a per-thread flag detects that the
26 // calling thread is already draining a pool job and runs the nested call
27 // as a plain sequential loop instead of touching the shared job state a
28 // second time, on whichever thread hits it (the original caller or a
29 // worker).
30
31 #include <atomic>
32 #include <cstddef>
33 #include <functional>
34 #include <thread>
35 #include <vector>
36
37 namespace brotensor::detail::cpu {
38
39 class ThreadPool {
40 public:
41 static ThreadPool& instance();
42
43 void run(std::size_t n, const std::function<void(std::size_t)>& fn);
44
45 // Workers plus the calling thread, i.e. the effective parallelism a
46 // run() call can spread work across.
47 1 int num_threads() const { return static_cast<int>(workers_.size()) + 1; }
48
49 // Explicitly joins every worker thread. Callers should invoke this
50 // deterministically during their own shutdown sequence — the pool is
51 // a Meyers singleton, so its destructor only runs during the process's
52 // static-destruction phase, by which point every *other* thread has
53 // already been suspended by RtlExitUserProcess. A worker suspended
54 // mid-operation while holding some global lock (e.g. the Debug CRT's
55 // iterator-checking mutex, taken by any std::vector destructor) can
56 // then deadlock the main thread's own exit-time TLS destructors
57 // waiting on that same lock forever. Calling shutdown() early — before
58 // any of that starts — avoids the whole class of hazard. Idempotent:
59 // safe to call more than once (the eventual destructor call included).
60 void shutdown();
61
62 ThreadPool(const ThreadPool&) = delete;
63 ThreadPool& operator=(const ThreadPool&) = delete;
64
65 private:
66 ThreadPool();
67 ~ThreadPool();
68
69 void worker_loop();
70
71 std::vector<std::thread> workers_;
72
73 // Job state, valid only while a run() call is outstanding (single job
74 // in flight at a time — see class comment).
75 const std::function<void(std::size_t)>* fn_ = nullptr;
76 std::size_t n_ = 0;
77 std::atomic<std::size_t> cursor_{0};
78 std::atomic<int> outstanding_{0}; // workers still draining this job
79
80 // Bumped once per run() call; workers block in generation_.wait(seen)
81 // until it changes, then pick up the new job (or exit, on shutdown).
82 std::atomic<int> generation_{0};
83 std::atomic<bool> shutdown_{false};
84 };
85
86 1691 inline void parallel_for(std::size_t n, const std::function<void(std::size_t)>& fn) {
87 1691 ThreadPool::instance().run(n, fn);
88 1691 }
89
90 } // namespace brotensor::detail::cpu
91