Skip to content

Speed up raw IO for many-small-read workloads (deep learning) - #14209

Closed
bruAristimunha wants to merge 6 commits into
mne-tools:mainfrom
bruAristimunha:io-speedup-dl
Closed

Speed up raw IO for many-small-read workloads (deep learning)#14209
bruAristimunha wants to merge 6 commits into
mne-tools:mainfrom
bruAristimunha:io-speedup-dl

Conversation

@bruAristimunha

@bruAristimunha bruAristimunha commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Reference issue (if any)

None. Umbrella draft; being split into #14212#14213#14214#14215#14216 (merge in that order).

What does this implement/fix?

Speeds up raw IO for workloads that make many small reads (DL training loops drawing random 2 s windows). Best of 4 interleaved runs vs main, 128 ch × 1800 s @ 512 Hz fixtures:

EDF 1472 → ~300 µs · BDF 2484 → ~406 µs · BrainVision 973 → ~200 µs · FIF 1305 → ~250 µs per window read. Preloading through a managed memmap cache drops reopening a 944 MB recording from ~450 ms to ~15 ms and puts window reads at the numpy floor (~84 µs).

Bit-identical output except where noted in the part descriptions.

Additional information

AI disclosure: I directed the work and reviewed/tested everything; Claude Code (Opus 4.1) wrote most of the code under my direction. Benchmark harness on the branch (benchmarks/io_dl/), kept out of review scope.

- Raw.get_data: skip full times-axis allocation when tmin/tmax unset;
  resolve picks=None to arange directly instead of string machinery
- _picks_to_idx: fast return for integer arrays already in range
- _mult_cal_one: fuse gather+cast+calibration into one pass
- FIF reader: read simple numeric tags through a PID-keyed memory map;
  select buffer entries via searchsorted on bounds
- EDF/BDF reader: vectorized uniform-sfreq fast path writing straight
  into the output buffer (incl. uniform stim channels via legacy bitmask),
  optional numba kernels for window decode and int24 samples with numpy
  fallbacks (all outputs bit-identical to the previous implementation)
- BaseRaw._get_windows: internal batched window reader reusing one buffer

Benchmarks (128ch x 1800s @512Hz, random 2s windows): EDF/BDF/BrainVision/
FIF per-call latency reduced ~4.3-7x vs main; preloaded access now at the
numpy floor.
engine='edfio' parses EDF via the optional edfio package into a preloaded
Raw (uniform sampling rates; all channels EEG; no meas_date). Decoding
stacks digital samples once and applies calibration in two fused passes;
output matches the native engine within 1 ulp.
_gdf_edf_get_fid now returns PID-keyed LRU-cached binary handles wrapped in
a no-close proxy so reader context managers detach instead of closing, and
reuse seeks to position 0 to preserve fresh-open semantics. Removes the
per-call open/close pair on every windowed read.
Add decode_window_into numba kernel writing calibrated samples into a
strided destination slice, removing the per-chunk temporary and copy when
no projector/compensation is active. Byteswap big-endian chunks to native
before the kernel (real-world EDF); kernel is gated to the direct path so
projection handling keeps its exact legacy route.
Preloading into a memmap file now skips decoding entirely when a file with
the expected size already exists, and BaseRaw.__del__ no longer deletes
memmap-backed files so caches persist across sessions. Reopening a 944 MB
recording drops from ~450 ms of decoding to ~15 ms, and windowed reads via
the public API run at the preloaded-data floor (~84 us).
preload="memmap" now requests an automatically managed memory-map cache
keyed by source path/mtime/size: the first read decodes into it, later
reads (including from fresh processes) mmap it directly instead of
decoding again. Directory defaults to ~/.cache/mne/memmap and can be
redirected via the MNE_MEMMAP_DIR config/environment variable.
@larsoner

Copy link
Copy Markdown
Member

For EDF/BDF I wonder if we can optimize our own reading such that edfio backend doesn't really help. IIRC that's a pure-Python package so it suggests we have some inefficiencies we can maybe fix

@bruAristimunha

Copy link
Copy Markdown
Contributor Author

Yes, we can @larsoner, I will split this PR into four steps, and leave the other dependencies out.

@bruAristimunha

Copy link
Copy Markdown
Contributor Author

Split into a dependency-stacked series of focused drafts (merge in order; each stacks on the previous):

  1. Speed up small reads on the Raw.get_data hot path #14212 — shared hot path (get_data lane, fused _mult_cal_one, batched _get_windows)
  2. Preserve caller-owned memory-mapped Raw preload files #14213 — FIF tags via memory map (depends on Speed up small reads on the Raw.get_data hot path #14212)
  3. Vectorized EDF/BDF window decode fast path #14214 — EDF/BDF vectorized fast path (+ persistent handles) (Preserve caller-owned memory-mapped Raw preload files #14213)
  4. Add optional edfio parsing engine to read_raw_edf #14215 — optional edfio engine (Vectorized EDF/BDF window decode fast path #14214)
  5. Persistent managed memmap caches + preload="memmap" sentinel #14216 — persistent managed memmap caches + preload="memmap" sentinel (Add optional edfio parsing engine to read_raw_edf #14215)

Each branch contains only its own files/hunks and passes its relevant suites; this umbrella stays as the overview until all parts merge.

@larsoner

Copy link
Copy Markdown
Member

I think we can close this and work with the individual PRs, but feel free to reopen if I'm mistaken!

@larsoner larsoner closed this Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants