Skip to content

Process-parallel DataLoader (num_workers) support for Dataset#64

Merged
CarloLucibello merged 8 commits into
mainfrom
cl/dataloader-numworkers-guide-and-fix
Jul 6, 2026
Merged

Process-parallel DataLoader (num_workers) support for Dataset#64
CarloLucibello merged 8 commits into
mainfrom
cl/dataloader-numworkers-guide-and-fix

Conversation

@CarloLucibello

@CarloLucibello CarloLucibello commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Enables and documents process-parallel data loading for Dataset: MLUtils.DataLoader(ds; num_workers=N) spreads getobs over N worker processes, each with its own CPython interpreter and GIL, so Python reads scale in a way the shared-GIL thread path (parallel=true) cannot. Works over mapobs/ObsView-wrapped datasets too.

How it works

A Dataset already serializes across a process boundary via datasets' own pickle — by reference to its on-disk Arrow cache_files, re-mmapped on the worker (data shared, not copied); in-memory datasets are materialized to a temporary Arrow dir once (fingerprint-cached). The requirement specific to distributed loading — that the pickle run on the GIL-holding main task, and that this package be loaded on the workers — is handled upstream in MLUtils (≥ 0.4.11), so it composes with mapobs/ObsView at any nesting with nothing dataset-specific on the loader side.

What's in the PR

  • num_workers works out of the box. DataLoader(ds; num_workers=N) (and over mapobs/ObsView) loads in parallel worker processes. Requires MLUtils ≥ 0.4.11.
  • Worker startup fix. __init__ pins JULIA_PYTHONCALL_EXE to the already-resolved interpreter when the env is CondaPkg-managed, so the N workers don't each re-resolve the CondaPkg environment on startup (they'd otherwise serialize on CondaPkg's file lock and redo work the parent already did).
  • Guide. Expanded Data Loaders section, split into Iterating batches / Materializing vs. on-the-fly / Parallel loading (num_workers), with two runnable doctests (batch structure and the mapobs(input, target) pattern) alongside Hub and num_workers examples.
  • Benchmark example. examples/flux_mnist.jl → an examples/flux_mnist/ directory: a Flux + HuggingFaceDatasets MNIST script plus 1:1 and idiomatic PyTorch counterparts, and a README comparing data-loading strategies (serial / materialized / thread-parallel / process-parallel) across both stacks.
  • @info when an in-memory dataset is first materialized to a temporary Arrow file for serialization.
  • Tests covering num_workers process-parallel loading.
  • Small serialization.jl refactor (_pickle_bytes / _unpickle_py helpers).
  • CHANGELOG: split [Unreleased] into [0.4.0] / [0.4.1] / [Unreleased], with the num_workers entry under [Unreleased].

🤖 Generated with Claude Code

@CarloLucibello CarloLucibello marked this pull request as draft July 3, 2026 10:56
@CarloLucibello CarloLucibello force-pushed the cl/dataloader-numworkers-guide-and-fix branch from 97da41a to 7f5d946 Compare July 3, 2026 15:59
@CarloLucibello CarloLucibello marked this pull request as ready for review July 4, 2026 05:11
@CarloLucibello CarloLucibello force-pushed the cl/dataloader-numworkers-guide-and-fix branch from 7f5d946 to 3e208fd Compare July 4, 2026 05:46
CarloLucibello and others added 8 commits July 6, 2026 09:50
`MLUtils.DataLoader(ds::Dataset; num_workers=N)` (MLUtils >= 0.4.10) spreads `getobs`
over N worker processes, each with its own CPython interpreter and GIL, so reads scale
where thread parallelism cannot (a shared GIL serializes them).

The loader serializes its data container from a background feeder task that does not hold
the GIL; a `Dataset` serializes by calling `pickle`/PythonCall, which segfaults off the
main task. Fix it with a `DistributedDataset` wrapper that precomputes the pickle bytes on
the calling (GIL-holding) task and ships those bytes — pure Julia, safe from any thread —
reconstructing a live `Dataset` on the worker. The `DataLoader(::Dataset)` method installs
the wrapper automatically when `num_workers > 0`; serial/`parallel` loaders are untouched.
`Dataset` itself is unchanged. An `@info` notes when an in-memory dataset is first
materialized to a temporary Arrow file so it can pickle by reference.

Tests: a DistributedDataset round-trip plus an end-to-end num_workers DataLoader over an
in-memory dataset; MLUtils is added as a test dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the combined MLUtils section into a focused integration section and a standalone
"Data loaders" section covering feeding a Dataset to DataLoader, on-the-fly vs. materialized
loading, and process-parallel `num_workers` loading (with DistributedDataset). Document
DistributedDataset in the API reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GIL-safe, feeder-thread-proof serialization needed for
`DataLoader(ds; num_workers=N)` now lives in MLUtils (>= 0.4.11): it
serializes the data container on the main task and loads this package on
the workers. That makes the interim `DistributedDataset` wrapper and the
`DataLoader(::Dataset)` hook redundant here, and additionally makes
`mapobs`/`ObsView`-wrapped datasets work under `num_workers`.

- Remove src/dataloader.jl (DistributedDataset + the DataLoader hook)
- Drop the export and the now-unused `numobs` import
- Bump MLUtils compat to 0.4.11 (main + test)
- Update guide, API docs, changelog, and tests accordingly

The `Dataset` Serialization methods (pickle-by-reference) are unchanged --
they are exactly what MLUtils' main-task serialization invokes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix the `[Data Loaders](@ref)` cross-reference (header case) that failed
  the Documentation CI job.
- Trim the guide's Data Loaders section to defer loader mechanics to the
  MLUtils guide; keep only the `Dataset`-specific notes.
- Move the Flux MNIST example into examples/flux_mnist/ with its own
  Project.toml, and switch it to process-parallel loading via `num_workers`
  (package-free transform; one-hot/loss on the main process).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DataLoader(...; num_workers=N) spawns Distributed workers that each `using
PythonCall`. Without help, every worker re-resolves the same CondaPkg env in
lockstep: they serialize on CondaPkg's file lock (noisy "Waiting for lock to be
freed") and redo work the parent already did.

Exporting the already-resolved interpreter in `__init__` lets the workers (which
inherit ENV) use it directly and skip CondaPkg entirely. Mirrors the CI-only
hack inside PythonCall; guarded on `CTX.which === :CondaPkg` and with `get!` so a
user-set interpreter is left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Rename examples/flux_mnist/main.jl -> flux_mnist.jl and add a warm-up epoch, so
  the reported per-config timings exclude Julia's JIT compilation.
- Add PyTorch counterparts (a 1:1 port and an idiomatic HF version) and a README
  with the Julia vs PyTorch timing tables and takeaways (Apple M1 Pro, CPU, 4
  epochs). Headline: materializing into memory is the big win; num_workers/process
  parallelism does not pay off for this toy MLP.
- Point the example links in docs/src/guide.md at the new path.
- gitignore __pycache__; add DataStructures compat bound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0.4.1 gets the Features/ClassLabel schema views (#65) and MLCore ≥ 1.1
getobs(::Py) change (#66); 0.4.0 keeps the breaking-release content; the
num_workers DataLoader entry stays under Unreleased. Compare links updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the Data Loaders section into Iterating batches / Materializing /
num_workers, with two verified jldoctests (batch structure and the
mapobs -> (input, target) pattern) plus julia-repl Hub and num_workers
examples.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@CarloLucibello CarloLucibello force-pushed the cl/dataloader-numworkers-guide-and-fix branch from 0dac018 to 36e7da5 Compare July 6, 2026 08:25
@CarloLucibello CarloLucibello merged commit c43b311 into main Jul 6, 2026
9 of 10 checks passed
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