Allow parallel=n to set the number of loading workers - #230
Merged
Conversation
Extends the `parallel` kwarg on `DataLoader`/`eachobs` from `Bool` to
`Union{Bool,Integer}`:
- `false`/`0` → serial (unchanged default)
- `true` → `Threads.nthreads()` workers (unchanged behaviour)
- `n::Integer` → exactly `n` worker tasks
This gives a direct lever over peak memory for parallel loading. Each
in-flight worker holds one (possibly batched) observation, so the
dominant memory term is `nworkers × batchsize`, not the prefetch channel
depth. Previously the worker count was hardcoded to `Threads.nthreads()`,
so a 16-thread session building 600 MB batches kept ~16-32 batches live
(see #210). With `parallel=2` a user caps that without dropping to fully
serial loading. Measured peak RSS on an 8-thread session, 36.8 MB
batches: parallel=true 1240 MB, =4 1129 MB, =2 799 MB, =1 551 MB
(serial floor ~583 MB).
The worker count is threaded through `eachobsparallel` → `Loader` →
`basesize = length ÷ nworkers`, and the prefetch `channelsize` now
defaults to `nworkers` (was `nthreads()`) so the channel buffer scales
with the worker count instead of staying pinned to the global thread
count. `parallel=true` resolves to `nthreads()`, so all defaults are
unchanged.
`_nworkers` is defined via dispatch rather than `==` so that
`Bool <: Integer` does not conflate `true` with `1`. The raw `parallel`
value is stored unchanged for faithful printing and reconstruction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CarloLucibello
force-pushed
the
cl/parallel-nworkers
branch
from
June 14, 2026 13:12
5289669 to
e10719c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Allow to set the number of parallel workers in the DataLoader (previously other serial behavior or num workers = num threads).
Fixes #210 (DataLoader parallel loading uses too much memory).
What
Extends the
parallelkwarg onDataLoader/eachobsfromBooltoUnion{Bool,Integer}:parallel=false/0trueThreads.nthreads()workers (current behavior)n::Integernworker tasks1This follows PyTorch's convention (
num_workers=0⇒ main-process/serial), folded onto the existing kwarg rather than adding a separatenum_workers— so there's no contradictory state likeparallel=false, num_workers=8.Why
In the parallel path each in-flight worker holds one whole (collated) batch, so peak memory is dominated by
nworkers × batchsize, not the prefetch channel depth. The worker count was hardcoded toThreads.nthreads(), so a 16-thread session building ~600 MB batches kept ~16–32 batches live → the 40 GB RSS in #210. An experiment confirmedchannelsizeis not a useful memory lever (lowering it didn't help, sometimes hurt), while the worker count is — so this exposes the worker count and leaveschannelsizederived from it.How
_nworkersresolves the kwarg, defined by dispatch (not==) soBool <: Integerdoesn't conflatetruewith1.eachobsparallel→Loader→basesize = length ÷ nworkers.channelsizenow defaults tonworkers(wasnthreads()), so the prefetch buffer scales with the worker count instead of the global thread count.parallelvalue is stored unchanged for faithful printing / reconstruction (parallel=truedoesn't silently round-trip toparallel=8).parallel=trueresolves tonthreads(), so all existing defaults and behavior are unchanged. WideningBool→Union{Bool,Integer}is backward compatible.Benchmark
Reproducing the #210 setup on a 16-core / 32-thread Threadripper (122 GiB RAM): 40 batches × 1024 images (224×224×3
Float32, ~600 MB/collated-batch),collate=true,rand-basedgetobs(CPU-bound, no I/O). Each cell is a fresh process soSys.maxrssis clean. (getobs!defined sobuffer=truereuses per-observation image buffers.)Peak RSS (GB):
parallelbufferfalsefalsefalsetrue1false1true2false2true4false4trueautofalseautotrueTime in seconds (GC% in parens):
parallelbufferfalsefalsefalsetrue1false1true2false2true4false4trueautofalseautotrueTakeaways:
parallel=nis the memory dial. Withbuffer=false, peak RSS tracksnworkers × batchsize: 2.5 (serial) → 2.4 (1) → 4 (2) → 6.6 (4) → 15.3 GB (auto@ 16 threads).parallel=1/2stay bounded regardless of thread count.autois what blows up — and worsens with thread count.buffer=trueis double-edged and must be paired with lownworkers. Serial /parallel=1: it's the lowest memory (1.5 GB, preallocation reuse, ~ the SimpleLoader in DataLoader uses way too much memory #210). Butauto, buffer=trueis the highest (23.5 GB), because the buffer pool is(nworkers+1)whole batches of per-observation buffers held for the whole iteration, on top of the in-flight collated outputs.getobsis pure CPU with no I/O to overlap and only 40 batches, soparallelbarely moves wall-clock here — treat RSS as the robust number. The consistent time win isbuffer=true(halves GC from ~21–29% to ~7–16%). On real I/O-bound loading, higherparallelwould help throughput, making the memory/throughput trade-off the actual decision.parallel=2, buffer=true— ~5.4 s, ~13% GC, ~4–5 GB, versusauto's 15–23 GB.Tests
New
parallel worker counttestset:_nworkersmapping incl. thetrue/1distinction,:serial/:parallelcodepath dispatch, raw-value round-trip, and that every observation is loaded exactly once forparallel ∈ (false, 0, 1, 2, true)across unbuffered / buffered / batched paths. Also verifiedparallel=ncomposes with thebuffer=true+collate=truepath from #216/#228 (stays@inferred-stable).🤖 Generated with Claude Code