pipe: blocking pipe_wait (COLI_PIPE_BLOCK=1) + PIPE_WORKERS implies PIPE=1
pipe_wait's sched_yield spin storms the scheduler for the full 0.5-3ms of each in-flight expert read; behind COLI_PIPE_BLOCK=1 it parks on a condvar instead (~5us wake, no lost-wakeup: workers store ready with release BEFORE taking the mutex to broadcast, and the waiter re-checks under the lock). Default OFF = byte-identical spin. Pthread pool only: the URING backend has no waiter spin to replace. Setting PIPE_WORKERS>0 in the env without PIPE now implies PIPE=1 with a stderr note: sizing the pool declares the intent to use it (a full benchmark campaign ran with PIPE_WORKERS=16 and the pipe silently off). The implication fires only when the platform default left the pipe off (no-op on _WIN32 where PIPE defaults to 1), only on a positive value (PIPE_WORKERS=0/empty does not enable a clamped 1-worker pipe), and an explicit PIPE=0 always wins. The rule lives in pipe_workers_imply_pipe() so the table is unit-testable. tests/test_pipe_block.c (in TEST_BINS, all platforms): pins the implication table, and drives the pool through 200 generations under each waiter against an on-disk expert fixture — spin as control, condvar arm alternating parked and fast-path waits — verifying identical slot bytes. Measured on the spin side (2x5090 + Gen5 NVMe, GLM-5.2 744B int4, CPU decode): 1.98 -> 2.16 tok/s at 192 tokens (expert-disk service 44.6s -> 33.5s). On Metal/GPU decode an M5 Pro A/B (PR thread) measured no change, consistent with the mechanism: the win is freeing the core the spinner was stealing from the CPU matmul team. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2146,6 +2146,22 @@ static int g_pipe=0; /* PIPE=1: async expert-load pipeline. Default ON for
|
||||
* the matmul. PIPE=0 opts back into the blocking serial path. */
|
||||
static int g_pipe_nw=8; /* PIPE_WORKERS=n: I/O worker threads (disk-parallel reads) */
|
||||
static int g_uring=0; /* URING=1: Linux io_uring load/completion backend; implies PIPE */
|
||||
static int g_pipe_block=0;/* COLI_PIPE_BLOCK=1: pipe_wait blocca su una condvar invece dello
|
||||
* spin sched_yield (default OFF = spin byte-identico). EN: a yield
|
||||
* storm on the main thread fights the OpenMP team for cycles during
|
||||
* multi-ms loads; the condvar wake costs ~5us against reads that
|
||||
* cost 0.5-3ms (#159). Pthread pool only: the URING backend has no
|
||||
* waiter spin to replace. */
|
||||
/* PIPE_WORKERS>0 esplicito nell'env implica PIPE=1: dimensionare il pool
|
||||
* dichiara l'intento di usarlo (una campagna intera l'ha impostato con la
|
||||
* pipe spenta senza accorgersene). EN: fires ONLY when PIPE is unset in the
|
||||
* env AND the platform default left the pipe off (on _WIN32 it already
|
||||
* defaults to 1) AND PIPE_WORKERS parses positive — the internal default of
|
||||
* 8 does not count, PIPE_WORKERS=0/empty does not, and an explicit PIPE=0
|
||||
* always wins. */
|
||||
static int pipe_workers_imply_pipe(const char *pipe_env, const char *pw_env, int pipe_now){
|
||||
return !pipe_now && !pipe_env && pw_env && atoi(pw_env)>0;
|
||||
}
|
||||
typedef struct {
|
||||
_Atomic uint64_t cur; /* (gen<<8)|index; gen main-only, index 0..njobs (≤64) */
|
||||
_Atomic int njobs; /* current batch job count */
|
||||
@@ -2153,6 +2169,7 @@ typedef struct {
|
||||
_Atomic int layer; /* current batch layer */
|
||||
_Atomic int ready[64]; /* per-slot load-done flag */
|
||||
pthread_mutex_t mx; pthread_cond_t cv; /* ONLY for parking/waking idle workers */
|
||||
pthread_cond_t cv_done; /* COLI_PIPE_BLOCK: signals ready[] transitions */
|
||||
Model *m;
|
||||
pthread_t th[16]; int nw; int started;
|
||||
} PipePool;
|
||||
@@ -2177,6 +2194,11 @@ static void *pipe_worker(void *arg){
|
||||
int eid=atomic_load_explicit(&p->eids[i],memory_order_relaxed); /* AFTER winning CAS */
|
||||
expert_load(p->m,L,eid,&p->m->ws[i],1); /* needed-now load: fatal on I/O error (matches serial path) */
|
||||
atomic_store_explicit(&p->ready[i],1,memory_order_release);
|
||||
if(g_pipe_block){ /* wake a main thread parked in pipe_wait */
|
||||
pthread_mutex_lock(&p->mx);
|
||||
pthread_cond_broadcast(&p->cv_done);
|
||||
pthread_mutex_unlock(&p->mx);
|
||||
}
|
||||
}
|
||||
/* CAS failed → another worker advanced index (or gen advanced): re-loop */
|
||||
}
|
||||
@@ -2194,6 +2216,7 @@ static void pipe_init(Model *m){
|
||||
g_pp.m=m; g_pp.nw=g_pipe_nw; if(g_pp.nw>16) g_pp.nw=16; if(g_pp.nw<1) g_pp.nw=1;
|
||||
atomic_store(&g_pp.cur,0); atomic_store(&g_pp.njobs,0);
|
||||
pthread_mutex_init(&g_pp.mx,NULL); pthread_cond_init(&g_pp.cv,NULL);
|
||||
pthread_cond_init(&g_pp.cv_done,NULL);
|
||||
for(int i=0;i<g_pp.nw;i++) pthread_create(&g_pp.th[i],NULL,pipe_worker,NULL);
|
||||
g_pp.started=1;
|
||||
}
|
||||
@@ -2228,6 +2251,17 @@ static inline void pipe_wait(int q){
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if(g_pipe_block){
|
||||
/* Fast path senza lock; poi ri-verifica SOTTO il lock prima di ogni
|
||||
* wait. EN: the worker stores ready (release) BEFORE it takes mx to
|
||||
* broadcast, so a set flag can never be missed (no lost wakeup). */
|
||||
if(atomic_load_explicit(&g_pp.ready[q],memory_order_acquire)) return;
|
||||
pthread_mutex_lock(&g_pp.mx);
|
||||
while(!atomic_load_explicit(&g_pp.ready[q],memory_order_acquire))
|
||||
pthread_cond_wait(&g_pp.cv_done,&g_pp.mx);
|
||||
pthread_mutex_unlock(&g_pp.mx);
|
||||
return;
|
||||
}
|
||||
while(!atomic_load_explicit(&g_pp.ready[q],memory_order_acquire)) sched_yield();
|
||||
}
|
||||
|
||||
@@ -6108,8 +6142,13 @@ int main(int argc, char **argv){
|
||||
0
|
||||
#endif
|
||||
;
|
||||
if(pipe_workers_imply_pipe(getenv("PIPE"),getenv("PIPE_WORKERS"),g_pipe)){
|
||||
g_pipe=1;
|
||||
fprintf(stderr,"[PIPE] PIPE_WORKERS is set — enabling the async pipe (PIPE=1 implied; set PIPE=0 to override)\n");
|
||||
}
|
||||
g_pipe_nw = getenv("PIPE_WORKERS")?atoi(getenv("PIPE_WORKERS")):8; /* I/O worker threads */
|
||||
if(g_pipe_nw<1) g_pipe_nw=1;
|
||||
g_pipe_block = getenv("COLI_PIPE_BLOCK")?atoi(getenv("COLI_PIPE_BLOCK")):0; /* blocking pipe_wait (default: spin) */
|
||||
g_direct = getenv("DIRECT")?atoi(getenv("DIRECT")):0;
|
||||
g_uring = getenv("URING")?atoi(getenv("URING")):0;
|
||||
if(g_uring){
|
||||
|
||||
Reference in New Issue
Block a user