Repository navigation
Add a refcounted thread pool keep-awake API - #9526
alexreinking wants to merge 2 commits into
Conversation
|
It's not clear to me why any thread pool logic at all needs to change beyond how the spin count is acquired. A too-large A team is very bad for performance, due to thundering herd issues. I don't think the A-team/B-team logic should be touched, I just think the threads waiting on the B team condition variable should be spinning inside the b team condition variable's wait method instead of sleeping. All the condition variables used support spinning, so just let them spin. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #9526 +/- ##
==========================================
- Coverage 71.05% 70.98% -0.08%
==========================================
Files 262 262
Lines 81084 81114 +30
Branches 19770 19777 +7
==========================================
- Hits 57615 57575 -40
- Misses 17502 17505 +3
- Partials 5967 6034 +67 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
6811645 to
b2ee07a
Compare
c9353a0 to
da28b61
Compare
b2ee07a to
e9ac133
Compare
Applies the thread_pool_common.h part of the upstream keep-awake proposal (#9526, e9ac133) verbatim to the app's copy of the pool: a refcounted count; while it is held, up to num_threads - 1 idle workers and owners waiting on their own loops poll for work instead of sleeping, idle workers are not demoted to the B team, an idle thread parks anyway after 4096 polls without work, and acquiring the first reference wakes both teams. The count survives halide_shutdown_thread_pool. The glue renames halide_thread_pool_keep_awake to ggml_halide_thread_pool_keep_awake, so it can't collide with the runtime's if #9526 lands, and runtime/thread_pool.h declares it with an RAII holder, ggml_halide::ThreadPoolKeepAwake. ctest ggml_thread_pool (runtime/thread_pool_test.cpp, after #9526's thread_pool_keep_awake_aottest) stress-tests the copy through the q4_0 x q8_0 mul_mat kernel (exact integer results) and direct calls: nested loops, do_parallel_tasks with semaphores, error propagation, keep-awake on and off with gaps, thread-count changes, shutdown/restart, and concurrent callers. No timing assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applies the thread_pool_common.h part of the upstream keep-awake proposal (#9526, e9ac133) verbatim to the app's copy of the pool: a refcounted count; while it is held, up to num_threads - 1 idle workers and owners waiting on their own loops poll for work instead of sleeping, idle workers are not demoted to the B team, an idle thread parks anyway after 4096 polls without work, and acquiring the first reference wakes both teams. The count survives halide_shutdown_thread_pool. The glue renames halide_thread_pool_keep_awake to ggml_halide_thread_pool_keep_awake, so it can't collide with the runtime's if #9526 lands, and runtime/thread_pool.h declares it with an RAII holder, ggml_halide::ThreadPoolKeepAwake. ctest ggml_thread_pool (runtime/thread_pool_test.cpp, after #9526's thread_pool_keep_awake_aottest) stress-tests the copy through the q4_0 x q8_0 mul_mat kernel (exact integer results) and direct calls: nested loops, do_parallel_tasks with semaphores, error propagation, keep-awake on and off with gaps, thread-count changes, shutdown/restart, and concurrent callers. No timing assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applies the thread_pool_common.h part of the upstream keep-awake proposal (#9526, e9ac133) verbatim to the app's copy of the pool: a refcounted count; while it is held, up to num_threads - 1 idle workers and owners waiting on their own loops poll for work instead of sleeping, idle workers are not demoted to the B team, an idle thread parks anyway after 4096 polls without work, and acquiring the first reference wakes both teams. The count survives halide_shutdown_thread_pool. The glue renames halide_thread_pool_keep_awake to ggml_halide_thread_pool_keep_awake, so it can't collide with the runtime's if #9526 lands, and runtime/thread_pool.h declares it with an RAII holder, ggml_halide::ThreadPoolKeepAwake. ctest ggml_thread_pool (runtime/thread_pool_test.cpp, after #9526's thread_pool_keep_awake_aottest) stress-tests the copy through the q4_0 x q8_0 mul_mat kernel (exact integer results) and direct calls: nested loops, do_parallel_tasks with semaphores, error propagation, keep-awake on and off with gaps, thread-count changes, shutdown/restart, and concurrent callers. No timing assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Idle workers in the default thread pool spin for 40 polls and then sleep, so back-to-back small parallel loops separated by serial work pay a thread wake-up that grows with the gap between loops. This adds a way to keep the pool awake around such phases (e.g. benchmarks, inference loops): - halide_thread_pool_keep_awake(bool) acquires or releases a reference on a process-wide keep-awake count and returns the new count. While it's positive, idle workers (up to halide_get_num_threads() - 1) and owners waiting on their own parallel loops keep polling for work instead of sleeping. Idle workers that the most recent loop didn't need stay on the A team rather than demoting to the B team, so a big loop after a small one doesn't pay to wake them either. Workers blocked on semaphores still sleep. When the count returns to zero, spinners park on their next poll. As a backstop against leaked references, idle threads park anyway after 4096 polls (a few milliseconds). Acquiring the first reference wakes all sleeping idle workers. Releasing an unheld reference calls halide_error and returns halide_error_code_generic_error. The count survives halide_shutdown_thread_pool and is independent of halide_set_num_threads. - Halide::Runtime::ThreadPoolKeepAwake: header-only RAII holder (AOT). - Halide::ThreadPoolKeepAwake and JITSharedRuntime::thread_pool_keep_awake for JIT. The front end tracks references, so they may be taken before the shared runtime exists and carry over across JITSharedRuntime::release_all. - hl.ThreadPoolKeepAwake: a Python context manager wrapping the JIT holder. - The fake thread pool only maintains the count. Tests cover nested and concurrent holders, many small loops with and without gaps, small loops alternating with big ones while held, set_num_threads changes, shutdown while held, nested parallelism, do_parallel_tasks with semaphores, async pipelines, unbalanced releases, and the Python context manager. A performance test reports fork/join latency with and without keep-awake. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
e9ac133 to
c7c1369
Compare
Per review: leave the A-team/B-team logic alone. While the keep-awake count is held, every halide_cond_with_spinning waiter (A team, B team, owners) just keeps spinning, up to keep_awake_max_spins, instead of going to sleep. Drop the per-team kept-awake caps and the wake-up broadcast on the first reference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Done in 534821f. The A/B team logic is untouched now. While the count is held, Authored by GitHub Copilot |
Applies the thread_pool_common.h part of the upstream keep-awake proposal (#9526, e9ac133) verbatim to the app's copy of the pool: a refcounted count; while it is held, up to num_threads - 1 idle workers and owners waiting on their own loops poll for work instead of sleeping, idle workers are not demoted to the B team, an idle thread parks anyway after 4096 polls without work, and acquiring the first reference wakes both teams. The count survives halide_shutdown_thread_pool. The glue renames halide_thread_pool_keep_awake to ggml_halide_thread_pool_keep_awake, so it can't collide with the runtime's if #9526 lands, and runtime/thread_pool.h declares it with an RAII holder, ggml_halide::ThreadPoolKeepAwake. ctest ggml_thread_pool (runtime/thread_pool_test.cpp, after #9526's thread_pool_keep_awake_aottest) stress-tests the copy through the q4_0 x q8_0 mul_mat kernel (exact integer results) and direct calls: nested loops, do_parallel_tasks with semaphores, error propagation, keep-awake on and off with gaps, thread-count changes, shutdown/restart, and concurrent callers. No timing assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a refcounted switch that keeps the default thread pool's idle workers spinning instead of sleeping, the shape agreed in maintainer discussion (a refcounted bit rather than a spin count). Without it, back-to-back small parallel loops with serial work between them pay a thread wake-up every time.
int halide_thread_pool_keep_awake(bool)acquires or releases a reference and returns the new count. While it's held, every thread waiting on the pool's spinning condition variables (A team, B team, and owners waiting on their own loops) keeps spinning instead of sleeping. The A/B team logic is unchanged. Workers blocked on async semaphores still sleep. An unbalanced release is an error.Halide::Runtime::ThreadPoolKeepAwake(AOT, header-only),Halide::ThreadPoolKeepAwake(JIT) andhl.ThreadPoolKeepAwake(Python,with).Future work: the remaining per-loop cost is the work queue's global mutex; a lock-free
do_par_forfast path would be a separate change.Tests: new
generator_aot_thread_pool_keep_awake,correctness_thread_pool_keep_awakeandpython_correctness_thread_pool_keep_awake;performance_thread_pool_keep_awakereports timings.Fork/join time per call with a 100 µs gap between loops (
performance_thread_pool_keep_awake, M3 Max, median of 3):Authored by GitHub Copilot