@@ -220,8 +220,6 @@ def _read_segments_file(
220220 if n_channels is None :
221221 n_channels = raw ._raw_extras [fi ]["orig_nchan" ]
222222
223- import os as _os
224-
225223 n_bytes = np .dtype (dtype ).itemsize
226224 # data_offset and data_left count data samples (channels x time points),
227225 # not bytes.
@@ -235,32 +233,18 @@ def _read_segments_file(
235233 # Reuse a memory map across calls (keyed by PID so forked processes --
236234 # e.g., PyTorch DataLoader workers -- create their own mapping instead of
237235 # sharing one). This removes the per-call open/seek/syscall overhead.
238- ex = raw ._raw_extras [fi ] if fi < len (raw ._raw_extras ) else {}
239- mm = ex .get ("_mm" ) if isinstance (ex , dict ) else None
240- if mm is not None and ex .get ("_mm_pid" ) != _os .getpid ():
241- mm = None
242- if mm is not None and (
243- mm .dtype != np .dtype (dtype )
244- or mm .size * n_bytes < data_offset + data_left * n_bytes
245- ):
236+ extras = raw ._raw_extras [fi ] if fi < len (raw ._raw_extras ) else {}
237+ mm = _memmap_for (extras , raw .filenames [fi ]) if isinstance (extras , dict ) else None
238+ if mm is not None and mm .size < data_offset + data_left * n_bytes :
246239 mm = None
247- if mm is None and isinstance (ex , dict ):
248- try :
249- mm = np .memmap (raw .filenames [fi ], dtype = dtype , mode = "r" )
250- ex ["_mm" ] = mm
251- ex ["_mm_pid" ] = _os .getpid ()
252- except Exception :
253- mm = None
254240
255241 if mm is not None :
256- base_idx = data_offset // n_bytes
257242 for sample_start in np .arange (0 , data_left , block_size ) // n_channels :
258243 count = min (block_size , data_left - sample_start * n_channels )
259- block = mm [
260- base_idx + sample_start * n_channels : base_idx
261- + sample_start * n_channels
262- + count
263- ]
244+ byte_start = data_offset + sample_start * n_channels * n_bytes
245+ block = np .frombuffer (
246+ mm [byte_start : byte_start + count * n_bytes ], dtype = dtype , count = count
247+ )
264248 if block .size != count :
265249 raise RuntimeError (
266250 f"Incorrect number of samples ({ block .size } != { count } ), "
@@ -383,23 +367,52 @@ def _make_split_fnames(fname, n_splits, split_naming):
383367 return res
384368
385369
370+ class _MemmapCache :
371+ """Hold a memory map without copying or pickling its contents."""
372+
373+ def __init__ (self ):
374+ self .mapping = None
375+ self .pid = None
376+
377+ def __deepcopy__ (self , memodict ):
378+ """Create an empty cache when its owner is copied."""
379+ return type (self )()
380+
381+ def __reduce__ (self ):
382+ """Create an empty cache when its owner is pickled."""
383+ return type (self ), ()
384+
385+ def close (self ):
386+ """Close the mapping and reset the cache."""
387+ mapping = self .mapping
388+ self .mapping = self .pid = None
389+ if mapping is not None :
390+ mapping ._mmap .close ()
391+
392+
386393def _memmap_for (extras , fname ):
387- """Return a PID-keyed read-only uint8 memmap of *fname* from *extras*.
388-
389- The mapping is created lazily on first call and cached in *extras* (a
390- per-instance dict) together with the PID that created it, so forked worker
391- processes build their own mapping instead of sharing inherited state.
392- Returns None if the file cannot be mapped. There is deliberately no
393- staleness check: callers index the mapping through tables read at open
394- time (bounds, entries), which are invalid if the file changes anyway,
395- so a per-call stat would only add overhead.
394+ """Return a process-local read-only uint8 memmap of *fname* from *extras*.
395+
396+ The mapping is created lazily on first call and held by a cache that is
397+ reset when *extras* is copied or pickled. The creating PID is recorded so
398+ forked worker processes build their own mapping instead of sharing
399+ inherited state. Returns None if the file cannot be mapped. There is
400+ deliberately no staleness check: callers index the mapping through tables
401+ read at open time (bounds, entries), which are invalid if the file changes
402+ anyway, so a per-call stat would only add overhead.
396403 """
397- mm = extras .get ("_mm" )
398- if mm is not None and extras .get ("_mm_pid" ) == os .getpid ():
399- return mm
404+ cache = extras .get ("_memmap_cache" )
405+ if cache is None :
406+ cache = extras ["_memmap_cache" ] = _MemmapCache ()
407+ mm = cache .mapping
408+ pid = os .getpid ()
409+ if mm is not None :
410+ if cache .pid == pid :
411+ return mm
412+ cache .close ()
400413 try :
401414 mm = np .memmap (str (fname ), dtype = np .uint8 , mode = "r" )
402415 except Exception :
403416 return None
404- extras [ "_mm" ], extras [ "_mm_pid" ] = mm , os . getpid ()
417+ cache . mapping , cache . pid = mm , pid
405418 return mm
0 commit comments