253 lines
8.6 KiB
C
253 lines
8.6 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/*
|
|
* threadpool.c — Foundry worker thread pool
|
|
*
|
|
* Mutex + condvar based implementation for reliable synchronization.
|
|
* Workers sleep on condvar when idle, wake on fw_for() signal.
|
|
*/
|
|
|
|
#include "threadpool/threadpool.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <stdatomic.h>
|
|
#include <pthread.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
struct fw_pool {
|
|
pthread_t *threads;
|
|
int n_threads; /* number of workers (excludes main) */
|
|
|
|
/* Work descriptor — set by fw_for(), read by workers */
|
|
fw_for_fn fn;
|
|
void *arg;
|
|
int count;
|
|
int chunk_size;
|
|
atomic_int next_chunk; /* atomic counter for chunk dispatch */
|
|
|
|
/* Synchronization */
|
|
pthread_mutex_t mutex;
|
|
pthread_cond_t work_ready; /* signal: new work available */
|
|
pthread_cond_t work_done; /* signal: all workers finished */
|
|
atomic_int generation; /* incremented each fw_for() call (lock-free spin) */
|
|
int workers_waiting; /* number of workers idle */
|
|
atomic_int active; /* threads still processing */
|
|
atomic_int shutdown;
|
|
};
|
|
|
|
/* Bounded spin before parking on the condvar. Back-to-back fw_for() dispatches
|
|
* (e.g. ~150 quantized matvecs per decode token) arrive within microseconds of
|
|
* each other, so a short spin lets workers pick up the next generation via the
|
|
* atomic counter and skip the ~20us condvar wake/park round-trip entirely. When
|
|
* work stops arriving the spin is exhausted and the worker parks on the condvar,
|
|
* preserving the original zero-CPU idle behaviour for latency-insensitive users
|
|
* of this shared pool. */
|
|
#ifndef FW_SPIN_BUDGET
|
|
#define FW_SPIN_BUDGET 240000
|
|
#endif
|
|
|
|
static inline void fw_cpu_relax(void) {
|
|
#if defined(__aarch64__) || defined(__arm__)
|
|
__asm__ __volatile__("yield" ::: "memory");
|
|
#elif defined(__x86_64__) || defined(__i386__)
|
|
__asm__ __volatile__("pause" ::: "memory");
|
|
#endif
|
|
}
|
|
|
|
static int detect_cores(void) {
|
|
#ifdef _WIN32
|
|
SYSTEM_INFO si;
|
|
GetSystemInfo(&si);
|
|
return (int)si.dwNumberOfProcessors;
|
|
#else
|
|
long n = sysconf(_SC_NPROCESSORS_ONLN);
|
|
return n > 0 ? (int)n : 1;
|
|
#endif
|
|
}
|
|
|
|
/* Execute chunks until none remain */
|
|
static void execute_chunks(fw_pool *pool) {
|
|
int cs = pool->chunk_size;
|
|
int total = pool->count;
|
|
for (;;) {
|
|
int ci = atomic_fetch_add(&pool->next_chunk, 1);
|
|
int start = ci * cs;
|
|
if (start >= total) break;
|
|
int end = start + cs;
|
|
if (end > total) end = total;
|
|
pool->fn(start, end, pool->arg);
|
|
}
|
|
}
|
|
|
|
static void *worker_loop(void *arg) {
|
|
fw_pool *pool = (fw_pool *)arg;
|
|
int my_gen = 0;
|
|
|
|
for (;;) {
|
|
/* Phase 1: lock-free spin for the next generation. */
|
|
int spins = 0;
|
|
int cur = atomic_load_explicit(&pool->generation, memory_order_acquire);
|
|
while (cur == my_gen) {
|
|
if (atomic_load_explicit(&pool->shutdown, memory_order_relaxed)) return NULL;
|
|
if (++spins >= FW_SPIN_BUDGET) break;
|
|
fw_cpu_relax();
|
|
cur = atomic_load_explicit(&pool->generation, memory_order_acquire);
|
|
}
|
|
|
|
/* Phase 2: spin exhausted with no work — park on the condvar. */
|
|
if (cur == my_gen) {
|
|
pthread_mutex_lock(&pool->mutex);
|
|
while (!atomic_load_explicit(&pool->shutdown, memory_order_relaxed) &&
|
|
atomic_load_explicit(&pool->generation, memory_order_acquire) == my_gen) {
|
|
pool->workers_waiting++;
|
|
pthread_cond_wait(&pool->work_ready, &pool->mutex);
|
|
pool->workers_waiting--;
|
|
}
|
|
int done = atomic_load_explicit(&pool->shutdown, memory_order_relaxed);
|
|
pthread_mutex_unlock(&pool->mutex);
|
|
if (done) return NULL;
|
|
} else if (atomic_load_explicit(&pool->shutdown, memory_order_relaxed)) {
|
|
return NULL;
|
|
}
|
|
|
|
my_gen = atomic_load_explicit(&pool->generation, memory_order_acquire);
|
|
|
|
/* Do work */
|
|
execute_chunks(pool);
|
|
|
|
/* Signal completion (main may be spinning on active or parked). */
|
|
if (atomic_fetch_sub(&pool->active, 1) == 1) {
|
|
pthread_mutex_lock(&pool->mutex);
|
|
pthread_cond_signal(&pool->work_done);
|
|
pthread_mutex_unlock(&pool->mutex);
|
|
}
|
|
}
|
|
}
|
|
|
|
fw_pool* fw_pool_new(int num_threads) {
|
|
if (num_threads <= 0)
|
|
num_threads = detect_cores();
|
|
/* Workers = total - 1 (main thread participates) */
|
|
int n_workers = num_threads - 1;
|
|
if (n_workers < 0) n_workers = 0;
|
|
|
|
fw_pool *pool = (fw_pool *)calloc(1, sizeof(fw_pool));
|
|
if (!pool) return NULL;
|
|
|
|
pool->n_threads = n_workers;
|
|
atomic_init(&pool->next_chunk, 0);
|
|
atomic_init(&pool->active, 0);
|
|
atomic_init(&pool->generation, 0);
|
|
pool->workers_waiting = 0;
|
|
atomic_init(&pool->shutdown, 0);
|
|
pthread_mutex_init(&pool->mutex, NULL);
|
|
pthread_cond_init(&pool->work_ready, NULL);
|
|
pthread_cond_init(&pool->work_done, NULL);
|
|
|
|
if (n_workers > 0) {
|
|
pool->threads = (pthread_t *)calloc(n_workers, sizeof(pthread_t));
|
|
if (!pool->threads) {
|
|
pthread_mutex_destroy(&pool->mutex);
|
|
pthread_cond_destroy(&pool->work_ready);
|
|
pthread_cond_destroy(&pool->work_done);
|
|
free(pool);
|
|
return NULL;
|
|
}
|
|
for (int i = 0; i < n_workers; i++) {
|
|
if (pthread_create(&pool->threads[i], NULL, worker_loop, pool) != 0) {
|
|
pthread_mutex_lock(&pool->mutex);
|
|
atomic_store(&pool->shutdown, 1);
|
|
pthread_cond_broadcast(&pool->work_ready);
|
|
pthread_mutex_unlock(&pool->mutex);
|
|
for (int j = 0; j < i; j++)
|
|
pthread_join(pool->threads[j], NULL);
|
|
pthread_mutex_destroy(&pool->mutex);
|
|
pthread_cond_destroy(&pool->work_ready);
|
|
pthread_cond_destroy(&pool->work_done);
|
|
free(pool->threads);
|
|
free(pool);
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
return pool;
|
|
}
|
|
|
|
void fw_pool_free(fw_pool *pool) {
|
|
if (!pool) return;
|
|
pthread_mutex_lock(&pool->mutex);
|
|
atomic_store(&pool->shutdown, 1);
|
|
pthread_cond_broadcast(&pool->work_ready);
|
|
pthread_mutex_unlock(&pool->mutex);
|
|
for (int i = 0; i < pool->n_threads; i++)
|
|
pthread_join(pool->threads[i], NULL);
|
|
pthread_mutex_destroy(&pool->mutex);
|
|
pthread_cond_destroy(&pool->work_ready);
|
|
pthread_cond_destroy(&pool->work_done);
|
|
free(pool->threads);
|
|
free(pool);
|
|
}
|
|
|
|
void fw_for(fw_pool *pool, int count, fw_for_fn fn, void *arg) {
|
|
if (count <= 0) return;
|
|
|
|
/* Serial fallback when no pool */
|
|
if (!pool || pool->n_threads == 0) {
|
|
fn(0, count, arg);
|
|
return;
|
|
}
|
|
|
|
int total_threads = pool->n_threads + 1;
|
|
/* Oversplit slightly for load balance */
|
|
int cs = count / (total_threads * 4);
|
|
if (cs < 1) cs = 1;
|
|
|
|
/* Set up work descriptor */
|
|
pool->fn = fn;
|
|
pool->arg = arg;
|
|
pool->count = count;
|
|
pool->chunk_size = cs;
|
|
atomic_store(&pool->next_chunk, 0);
|
|
atomic_store(&pool->active, total_threads);
|
|
|
|
/* Publish work: the release-store on generation hands the descriptor above
|
|
* to spinning workers (they acquire-load generation). The broadcast then
|
|
* wakes any worker that already parked on the condvar. Spinning workers
|
|
* usually observe the new generation before the broadcast, skipping it. */
|
|
atomic_fetch_add_explicit(&pool->generation, 1, memory_order_release);
|
|
/* Always broadcast under the mutex so a worker that has just parked is never
|
|
* missed (skipping it conditionally would race the park path and deadlock).
|
|
* The lock is uncontended while workers spin, so this is a cheap ~100ns. */
|
|
pthread_mutex_lock(&pool->mutex);
|
|
pthread_cond_broadcast(&pool->work_ready);
|
|
pthread_mutex_unlock(&pool->mutex);
|
|
|
|
/* Main thread participates */
|
|
execute_chunks(pool);
|
|
|
|
/* Signal main's completion */
|
|
if (atomic_fetch_sub(&pool->active, 1) == 1) {
|
|
/* Main was the last — no need to wait */
|
|
return;
|
|
}
|
|
|
|
/* Bounded spin-join before parking: workers were woken lock-free and finish
|
|
* within microseconds, so the spin usually completes the join with no
|
|
* condvar round-trip. */
|
|
for (int s = 0; s < FW_SPIN_BUDGET; s++) {
|
|
if (atomic_load_explicit(&pool->active, memory_order_acquire) == 0) return;
|
|
fw_cpu_relax();
|
|
}
|
|
|
|
/* Fallback: park until all workers finish. */
|
|
pthread_mutex_lock(&pool->mutex);
|
|
while (atomic_load(&pool->active) > 0)
|
|
pthread_cond_wait(&pool->work_done, &pool->mutex);
|
|
pthread_mutex_unlock(&pool->mutex);
|
|
}
|