Skip to content

Keep compiling and garbage collection off the worker threads of mixed models - #65

Merged
parashardhapola merged 1 commit into
masterfrom
55-threads-and-garbage-collection
Oct 26, 2026
Merged

parashardhapola merged 1 commit into
masterfrom
55-threads-and-garbage-collection

Conversation

@parashardhapola

Copy link
Copy Markdown
Member

Summary

The mixed-model fits and Kenward and Roger's adjustment run their compiled routines on a
pool of Python threads. When Python's garbage collector starts on one of those threads,
it runs the finalizers of whatever it frees there, and some objects may only be finalized
on the thread that made them. The test suite showed it: on Python 3.14, Tk objects left
by matplotlib's interactive backend were finalized during a mixed-model fit and raised
"main thread is not in main loop". Once, a cold compile on a worker thread crashed the
interpreter inside the collector. Both routines now compile on the calling thread, and
the collector waits until the workers finish.

Changes

  • cytepool._jit.by_runs(run, n_items) (private): runs a routine that releases the GIL
    over runs of consecutive items, the first run on the calling thread (which compiles
    the routine, or loads it from the cache) and the others on a pool of Python threads,
    with the garbage collector paused until they finish and restored even when a run
    raises.
  • fit_mixed_model (through its private _fit) and Kenward and Roger's adjustment in
    cytepool.stats._fixed_effect_inference use it in place of their own thread pools.
    Each feature is computed by the same operations as before, so every result is
    unchanged to the bit.
  • tests/conftest.py: the tests draw figures with matplotlib's Agg backend, as on CI,
    instead of the interactive backend matplotlib chooses where a display is available.

Design notes

  • Why pause the collector rather than only compile first: compiling on the calling
    thread removes the compile from the workers, but a collection can still start on a
    worker at any allocation, such as the arguments of each call. Pausing it for the
    length of the pool keeps every finalizer on the calling thread. The workers allocate
    almost nothing that needs collecting, so memory does not grow.
  • The first run is about a quarter of one thread's share of the work, so the time lost
    to running it before the pool starts is small.
  • numba's own parallel loops (prange) run in native threads without Python state, so
    the routines that use them are not affected.

Tests

tests/test_jit.py gains three tests:

  • by_runs covers every item once, in eight runs for two threads, the first on the
    calling thread;
  • with one thread it runs everything on the calling thread;
  • with the collector running at almost every allocation, objects that only the
    collector frees, made on the workers, are all finalized on the calling thread. The
    same pool without the pause finalized 1,337 of the 1,600 on worker threads.

The mixed-model, Kenward-Roger, dream, design-check and variance-partitioning tests pass
unchanged (191 tests). The full suite on top of the pull requests before it: 4,926
passed, 18 skipped, also with bounds checks and with the rounding changed; Python 3.14:
4,947 passed, without the warning of the Tk objects; Python 3.15: 4,841 passed, 107
skipped.

@parashardhapola
parashardhapola force-pushed the 38b-combine-studies-guides branch from 5f5ed6d to b6f80ac Compare October 26, 2026 23:14
@parashardhapola
parashardhapola changed the base branch from 38b-combine-studies-guides to master October 26, 2026 23:18
… models

The mixed-model fits and Kenward and Roger's adjustment run their compiled
routines on Python threads. A garbage collection that starts on one of those
threads runs the finalizers of what it frees there, and some objects may only
be finalized on the thread that made them: Tk's, which matplotlib's
interactive backend makes, raised errors in the test suite, and a cold compile
on a worker thread once crashed the interpreter in the collector. The shared
helper by_runs now runs the first run on the calling thread, so the routine
compiles there, and pauses the collector while the workers run. The tests
draw figures with matplotlib's Agg backend, as CI does.
@parashardhapola
parashardhapola marked this pull request as ready for review October 26, 2026 23:18
@parashardhapola
parashardhapola force-pushed the 55-threads-and-garbage-collection branch from 45b1bb4 to 9bb9150 Compare October 26, 2026 23:18
@parashardhapola
parashardhapola merged commit dc964ff into master Oct 26, 2026
6 checks passed
@parashardhapola
parashardhapola deleted the 55-threads-and-garbage-collection branch October 26, 2026 23:23
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.

1 participant