Cropped inference: trials, chunks and lifecycle#

predict_trials() returns every trial at once. iter_trial_predictions() yields one trial at a time and iter_trial_prediction_chunks() yields TrialPredictionChunk records of at most windows_per_chunk windows. All three use the same chronological overlap reconstruction. For a fitted classifier or regressor, pass its module_ to the iterators.

Memory#

predict_trials keeps all outputs. iter_trial_predictions keeps one trial, so its memory still grows with the trial length. iter_trial_prediction_chunks keeps one chunk, so its memory does not depend on the trial length. Both iterators also hold the loader batches and one window of lookahead. The bound only holds if each output is discarded after use: list(iterator) materializes everything again.

Targets and positions#

Predictions and targets are float32; a trial prediction has shape (n_outputs, n_predictions). A scalar or vector target is the trial’s first-window target, repeated in every chunk. A time target (at least two dimensions per window) is reconstructed like the predictions. With return_targets=False the trial iterator yields arrays and chunk targets are None.

prediction_start is the zero-based offset of a chunk in the trial’s reconstructed predictions, not an EEG sample index. Concatenating a trial’s chunks along axis 1 reproduces the trial iterator’s output exactly, for predictions and time targets.

Lifecycle and early close#

The iterators are lazy. On first advancement they record each submodule’s training flag and switch the model to eval mode; exhaustion, an error or close() shuts down loader workers and restores the flags. torch.no_grad covers only the forward. Do not train the model while an iterator is suspended. A break alone does not close a generator, so wrap early exits in contextlib.closing:

>>> import torch
>>> from contextlib import closing
>>> from braindecode.training import iter_trial_prediction_chunks
>>> model = torch.nn.Conv1d(1, 1, kernel_size=1, bias=False)
>>> _ = model.train()
>>> windows = [(torch.ones(1, 4), 7, (i, 4 * i, 4 * (i + 1))) for i in range(2)]
>>> with closing(
...     iter_trial_prediction_chunks(
...         model,
...         windows,
...         windows_per_chunk=1,
...         return_targets=False,
...     )
... ) as chunks:
...     chunk = next(chunks)
...     print(
...         chunk.trial_index,
...         chunk.prediction_start,
...         chunk.prediction.shape,
...         chunk.target,
...         model.training,
...     )
...
0 0 (1, 4) None False
>>> model.training
True

Chronological DataLoader controls#

loader_kwargs accepts only batch_size, num_workers, pin_memory, persistent_workers, prefetch_factor, worker_init_fn and generator, and overrides the matching function arguments. Shuffling, samplers, drop_last, custom collation, batch_size=None and any other key are rejected, because reconstruction relies on chronological batches. Batches with channel positions are not supported: disable return_ch_pos. Loader settings are not copied from an estimator’s training loader.

Opt-in compilation and checkpoint migration#

EEGClassifier and EEGRegressor accept compile=True and skorch’s compile__* options. Compilation is opt-in and not guaranteed to be faster. It compiles the module in place, so supplied Parameters, shared parameters and state-dict keys stay the same, and save_params/load_params checkpoints load into eager models.

Checkpoints from skorch’s torch.compile wrapper have _orig_mod.-prefixed keys, which loading does not strip. Export _orig_mod.state_dict() from the wrapped module where it was saved, or remove only that verified prefix, then load the result strictly.

Pickle, joblib and deepcopy give an eager estimator; warm start and partial_fit do not recompile. initialize() compiles again but also reinitializes the module, so call load_params afterwards to continue training. An estimator only replaces or clears a compiled call it installed itself: turn compilation off through that estimator before reusing the module eagerly elsewhere.

Distributed evaluation recipe (not an estimator backend)#

benchmarks/bench_distributed_windows.py in the source repository shows how to evaluate preloaded windows across local CPU/Gloo processes, for example with python -m benchmarks.bench_distributed_windows --ranks 4 --output 4.json. Each rank forwards an unpadded rank-strided shard, keeping the global window index next to each prediction. Predictions, targets and crop indices are gathered together, the gather fails unless every window arrives exactly once, and only the chronologically ordered windows are reconstructed into trials and scored. Never average rank-local trial scores, and never pass a shuffled sampler to the chronological trial iterators.

The estimators themselves still predict in one process. Every rank holds the full dataset and the full gathered predictions, and process launch can cost more than the parallel evaluation saves in a one-shot run.