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 |