Skip to content

Read KIT data in cache-sized blocks - #14255

Merged
larsoner merged 3 commits into
mne-tools:mainfrom
bruAristimunha:perf/kit-block-size
Aug 30, 2026
Merged

Read KIT data in cache-sized blocks#14255
larsoner merged 3 commits into
mne-tools:mainfrom
bruAristimunha:perf/kit-block-size

Conversation

@bruAristimunha

Copy link
Copy Markdown
Contributor

What does this implement/fix?

The reader asked for up to 100 MB per block, then cast the whole block to
float64 and scaled it in place (.astype(float) then block *= conv_factor), so
the working set ran to several hundred MB and none of it stayed in cache.

Same treatment as #14241 and #14246.

Numbers

Median of 5 interleaved process pairs against a pristine main worktree:

file main this PR
136 MB (161 ch x 425000) 258.5 ms 145.6 ms 1.78x

A sweep puts the optimum at 2 MiB — 32 MiB is only 1.14x, 8 MiB 1.38x,
4 MiB 1.66x, 1 MiB 1.60x.

No shipped fixture regresses, and two improve because they were already being
split differently:

fixture main this PR
ArtificalSignalData_Yokogawa_1khz.con 2.87 ms 2.26 ms 1.27x
ArtificalSignalData_RICOH_1khz.con 3.06 ms 2.77 ms 1.10x
Example_PQA160C_1001-export.con 1.05 ms 1.07 ms 0.99x
010409_Motor_task_coregist-export.con 1.22 ms 1.24 ms 0.98x

Correctness

  • Output bit-identical to main on every readable KIT fixture (np.array_equal).
  • pytest mne/io/kit: 15 passed.

Caveats

The constant differs from the other readers (2 MiB here, 8 MiB BrainVision,
2–16 MiB in #14246) because the optimum tracks time points per block, so it
moves with the channel count. As on #14246, I think a max_block_samples=
parameter would express this better than a per-reader byte budget, and I am
happy to go that way instead.

No fixture in the testing dataset is large enough to show the effect, so CI
cannot demonstrate it. The large file was synthesised by tiling the raw-data
section of data_berlin.con — verified to be the last section in the file — and
patching n_samples in the acquisition header.

Additional information

AI disclosure: I directed the work and reviewed and tested every change; Claude
Code (Claude Opus 5) ran the block-size sweep and the A/B measurements and made
the edits under my direction.

@larsoner

Copy link
Copy Markdown
Member

I think a max_block_samples=parameter would express this better than a per-reader byte budget, and I am happy to go that way instead.

Ahh yes I see how this could be dependent on the number of channels (and/or number of samples per stored block) for some data formats... if it makes sense, instead of this being a module-level thing, it could be computed in the __init__ and stored in the extras for use in the reading function. Do you want to try it here to see if there is some benefit for KIT or other readers? If it's only < 10 changed lines per reader, okay for it to be part of this PR I think. (No need for additional changelog entries since they already exist for the other readers already, this is just tweaking previous PRs within a release cycle.)

The reader asked for up to 100 MB per block, then cast the whole block to
float64 and scaled it in place, so the working set was several hundred MB and
none of it stayed in cache.

  raw.get_data(), interleaved cross-process medians:

    136 MB file (161ch x 425000)   258.5 -> 145.6 ms   1.78x

A sweep over 32/16/8/4/2/1 MiB puts the optimum at 2 MiB (32 MiB is only 1.14x,
8 MiB 1.38x, 4 MiB 1.66x, 1 MiB 1.60x).

No shipped fixture regresses; two of them improve because they were already
being split differently:

    ArtificalSignalData_Yokogawa_1khz.con   2.87 -> 2.26 ms   1.27x
    ArtificalSignalData_RICOH_1khz.con      3.06 -> 2.77 ms   1.10x
    Example_PQA160C_1001-export.con                           0.99x
    010409_Motor_task_coregist-export.con                     0.98x

Output is bit-identical on every readable KIT fixture. The large file was
synthesised by tiling the raw-data section of data_berlin.con -- it is the last
section in the file -- and patching n_samples in the acquisition header.

Same treatment as mne-tools#14241 and mne-tools#14246; the constant differs because the optimum
tracks time points per block, so it moves with the channel count.
@bruAristimunha

Copy link
Copy Markdown
Contributor Author

Updated this to compute block lengths in time samples during reader initialization and store them in raw_extras. The same small change is included for BrainVision, Artemis123, Curry, EEGLAB, Eximia, FIL, NSx, Persyst, and Nihon Kohden; the shared reader now accepts max_block_samples. EGI is unchanged because its event-block calculation already occurs once during initialization. The affected reader suites pass locally.

@larsoner

Copy link
Copy Markdown
Member

Any big speedups from those or did they end up being subtle?

@bruAristimunha

Copy link
Copy Markdown
Contributor Author

hey @larsoner,

  • KIT: 300.4 → 162.3 ms, 1.85×
  • Nihon: 278.8 → 155.3 ms, 1.79×
  • Persyst: 100.0 → 40.2 ms, 2.46×

@larsoner

Copy link
Copy Markdown
Member

Love it! Thanks @bruAristimunha

@larsoner
larsoner merged commit dcd3ae8 into mne-tools:main Aug 30, 2026
31 checks passed
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.

3 participants