Skip to content

Add a refcounted thread pool keep-awake API - #9526

Open
alexreinking wants to merge 2 commits into
mainfrom
alexreinking/thread-pool-keep-awake
Open

alexreinking wants to merge 2 commits into
mainfrom
alexreinking/thread-pool-keep-awake

Conversation

@alexreinking

@alexreinking alexreinking commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

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.
  • Backstop: an idle thread parks anyway after 4096 polls without work (a few ms), which bounds the cost of a leaked holder.
  • RAII holders: Halide::Runtime::ThreadPoolKeepAwake (AOT, header-only), Halide::ThreadPoolKeepAwake (JIT) and hl.ThreadPoolKeepAwake (Python, with).
  • Behavior is unchanged while no reference is held, and custom thread pools are unaffected.

Future work: the remaining per-loop cost is the work queue's global mutex; a lock-free do_par_for fast path would be a separate change.

Tests: new generator_aot_thread_pool_keep_awake, correctness_thread_pool_keep_awake and python_correctness_thread_pool_keep_awake; performance_thread_pool_keep_awake reports timings.

Fork/join time per call with a 100 µs gap between loops (performance_thread_pool_keep_awake, M3 Max, median of 3):

threads default keep-awake
8 10.8 µs 1.5 µs
12 30.7 µs 0.8 µs
16 0.6 µs 0.5 µs

Authored by GitHub Copilot

@alexreinking
alexreinking added this pull request to stack #9527 October 7, 2026 19:26
@abadams

abadams commented Oct 7, 2026

Copy link
Copy Markdown
Member

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

codecov Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.98%. Comparing base (ee9f17a) to head (534821f).

Files with missing lines Patch % Lines
src/JITModule.cpp 90.00% 1 Missing and 2 partials ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@alexreinking
alexreinking removed this pull request from stack #9527 October 8, 2026 11:09
@alexreinking
alexreinking added this pull request to stack #9529 October 8, 2026 11:10
@alexreinking
alexreinking force-pushed the alexreinking/thread-pool-keep-awake branch from 6811645 to b2ee07a Compare October 8, 2026 13:55
@alexreinking
alexreinking force-pushed the alexreinking/arm-i8mm branch from c9353a0 to da28b61 Compare October 8, 2026 13:55
@alexreinking
alexreinking removed this pull request from stack #9529 October 8, 2026 14:37
@alexreinking
alexreinking force-pushed the alexreinking/thread-pool-keep-awake branch from b2ee07a to e9ac133 Compare October 8, 2026 14:38
@alexreinking
alexreinking changed the base branch from alexreinking/arm-i8mm to main October 8, 2026 14:38
alexreinking added a commit that referenced this pull request Oct 8, 2026
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>
alexreinking added a commit that referenced this pull request Oct 9, 2026
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>
alexreinking added a commit that referenced this pull request Oct 9, 2026
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>
@alexreinking
alexreinking force-pushed the alexreinking/thread-pool-keep-awake branch from e9ac133 to c7c1369 Compare October 9, 2026 15:19
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>
@alexreinking

Copy link
Copy Markdown
Member Author

Done in 534821f. The A/B team logic is untouched now. While the count is held, halide_cond_with_spinning::wait just keeps spinning, up to the 4096-poll backstop, instead of sleeping. That applies to every waiter: A team, B team and owners. I dropped the per-team caps and the wake-up broadcast on the first reference, so the only change to the pool is the spin loop's bound. The numbers in the description are re-measured; keep-awake is unchanged at 0.5–1.5 µs per fork/join.


Authored by GitHub Copilot

@alexreinking
alexreinking marked this pull request as ready for review October 9, 2026 15:51
alexreinking added a commit that referenced this pull request Oct 10, 2026
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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants