Files
colibri-strix/c
Rodolfo Hansen 03d9a23fe4 perf(moe): overlap NVMe expert loads with matmul via opt-in PIPE I/O pool (default off, oracle-exact) (#79)
For streamed (non-resident) experts the MoE forward is disk-bound: each
64-expert block first blocks on a parallel pread of every miss, then runs
the matmuls serially. Those two phases don't overlap, so the compute
cores idle while the block's weights are still coming off NVMe.

PIPE=1 hands the misses' expert_load() to a small persistent pool of I/O
worker pthreads and lets the main thread start matmul immediately. The
main thread walks the block's experts in routing order and waits on a
per-slot ready flag only for the expert it needs right now, so loads of
later experts in the block hide behind matmul of earlier ones. All
matmul_qt stays on the main thread (it parallelises internally via
OpenMP and gates GPU dispatch on !omp_in_parallel()), so the I/O pool
never competes with the compute team for the matmul itself.

Cross-generation correctness: generation-tagged lock-free cursor
--------------------------------------------------------------------
The batch state (njobs/eids[]/layer/ready[]) is reused in place across
64-blocks, so a straggler or late-woken worker could touch the NEXT
batch's state. Two prior fixes (a drain barrier, then an _Atomic active
counter) each still left a cross-generation window. Both are now removed
and replaced with a single generation-tagged cursor:

    _Atomic uint64_t cur = (gen << 8) | index;   // gen main-only, index 0..njobs

  - dispatch writes njobs/layer/eids[]/ready-reset with RELAXED stores,
    then RELEASE-stores cur (bumping gen); that release publishes all
    batch state to any worker whose ACQUIRE-load of cur sees the new gen.
  - a worker grabs a job by CAS-advancing the index; it reads eids[i]/
    layer only AFTER the winning CAS. The CAS comparand carries the
    generation, so if a new batch was published the stale CAS fails and
    the worker re-reads — it can never grab a wrong-generation job or
    read torn state, no matter where it was preempted (wake gap,
    post-cursor, anywhere).
  - gen is bumped only by the main thread and is monotonic ⇒ no ABA.
  - the per-expert pipe_wait(ready[q]) in the matmul loop (kept) makes
    every grabbed job complete before the block ends, so no grab
    outlives its generation. That is what makes the old `active`
    counter and the end-of-block drain barrier unnecessary — both are
    removed. ready[] is reset before the publishing release, so no stale
    flag survives into the new generation.
  - the mutex/condvar now exist ONLY to park and wake idle workers, not
    for correctness.

This only reorders I/O, never the computation: greedy decode output is
byte-identical to the blocking path.

Default OFF (PIPE=0) so upstream behaviour is unchanged; PIPE=1 opts in
and PIPE_WORKERS=n sizes the pool (default 8). No Makefile change: pure C
(stdatomic.h + sched.h), builds with the default toolchain.


Claude-Session: https://claude.ai/code/session_01DS7oc65c5RdA9V99otRCwt

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 13:44:34 +02:00
..