Skip to content

Commit 4c681a8

Browse files
committed
Reintorduce lazy loading but pickle the map only once per process in the pool
1 parent 4b9a500 commit 4c681a8

2 files changed

Lines changed: 77 additions & 44 deletions

File tree

src/mdio/segy/_workers.py

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66
from typing import TYPE_CHECKING
77

88
import numpy as np
9+
from segy import SegyFile
910
from segy.arrays import HeaderArray
11+
from zarr import open_group as zarr_open_group
1012

1113
from mdio.core.config import MDIOSettings
1214
from mdio.segy._raw_trace_wrapper import SegyFileRawTraceWrapper
1315
from mdio.segy.file import SegyFileArguments
1416
from mdio.segy.file import SegyFileWrapper
1517

1618
if TYPE_CHECKING:
17-
from numpy.typing import NDArray
18-
from segy import SegyFile
1919
from zarr import Array as zarr_Array
2020

2121
from zarr.core.config import config as zarr_config
@@ -69,32 +69,73 @@ def header_scan_worker(
6969
return HeaderArray(trace_header) # wrap back so we can use aliases
7070

7171

72-
def trace_worker( # noqa: PLR0913
73-
segy_file: SegyFile,
74-
data_array: zarr_Array,
75-
header_array: zarr_Array | None,
76-
raw_header_array: zarr_Array | None,
77-
region: dict[str, slice],
78-
local_grid_map: NDArray,
79-
) -> SummaryStatistics | None:
72+
# Per-worker process state populated once by `trace_worker_init`. Keeping the SEG-Y handle,
73+
# Zarr array handles, and the (compressed, in-memory) grid map here lets us pickle them a single
74+
# time per worker via the pool initializer instead of once per submitted block. The grid map is
75+
# retained as a compressed in-memory Zarr array and sliced lazily per region, so each worker only
76+
# materializes its own block rather than the full dense map.
77+
_worker_state: dict[str, object] = {}
78+
79+
80+
def trace_worker_init(
81+
segy_file_kwargs: SegyFileArguments,
82+
output_path: str,
83+
storage_options: dict[str, object] | None,
84+
use_consolidated: bool,
85+
data_variable_name: str,
86+
grid_map: zarr_Array,
87+
) -> None:
88+
"""Initialize per-process state for trace ingestion workers.
89+
90+
Used as the `ProcessPoolExecutor` initializer so the SEG-Y file, Zarr output handles, and grid
91+
map are opened/transferred once per worker process rather than re-pickled for every block.
92+
93+
Args:
94+
segy_file_kwargs: Arguments to open the SegyFile instance.
95+
output_path: POSIX path to the output MDIO Zarr store.
96+
storage_options: fsspec storage options for the output store.
97+
use_consolidated: Whether to open the group with consolidated metadata (Zarr V2).
98+
data_variable_name: Name of the data variable in the dataset.
99+
grid_map: Compressed in-memory Zarr array mapping live traces to their positions.
100+
"""
101+
# Setting the zarr config to 1 thread to ensure we honor the `MDIO__IMPORT__CPU_COUNT` environment variable.
102+
# The Zarr 3 engine utilizes multiple threads. This can lead to resource contention and unpredictable memory usage.
103+
zarr_config.set({"threading.max_workers": 1})
104+
105+
zarr_group = zarr_open_group(
106+
output_path,
107+
mode="r+",
108+
storage_options=storage_options,
109+
use_consolidated=use_consolidated,
110+
)
111+
112+
_worker_state["segy_file"] = SegyFile(**segy_file_kwargs)
113+
_worker_state["data_array"] = zarr_group[data_variable_name]
114+
_worker_state["header_array"] = zarr_group.get("headers")
115+
_worker_state["raw_header_array"] = zarr_group.get("raw_headers")
116+
_worker_state["grid_map"] = grid_map
117+
118+
119+
def trace_worker(region: dict[str, slice]) -> SummaryStatistics | None:
80120
"""Writes a subset of traces from a region of the dataset of Zarr file.
81121
122+
Reads its shared inputs (SEG-Y handle, Zarr arrays, grid map) from the per-process state set up
123+
by `trace_worker_init`, so only the lightweight `region` is pickled per block.
124+
82125
Args:
83-
segy_file: The opened SEG-Y file.
84-
data_array: Zarr array for writing trace data.
85-
header_array: Zarr array for writing trace headers (or None if not needed).
86-
raw_header_array: Zarr array for writing raw headers (or None if not needed).
87126
region: Region of the dataset to write to.
88-
local_grid_map: Sliced numpy array mapping live traces to their positions.
89127
90128
Returns:
91129
SummaryStatistics object containing statistics about the written traces.
92130
"""
93-
# Setting the zarr config to 1 thread to ensure we honor the `MDIO__IMPORT__CPU_COUNT` environment variable.
94-
# The Zarr 3 engine utilizes multiple threads. This can lead to resource contention and unpredictable memory usage.
95-
zarr_config.set({"threading.max_workers": 1})
131+
segy_file: SegyFile = _worker_state["segy_file"]
132+
data_array: zarr_Array = _worker_state["data_array"]
133+
header_array: zarr_Array | None = _worker_state["header_array"]
134+
raw_header_array: zarr_Array | None = _worker_state["raw_header_array"]
135+
grid_map: zarr_Array = _worker_state["grid_map"]
96136

97137
region_slices = tuple(region.values())
138+
local_grid_map = grid_map[region_slices[:-1]] # minus last (vertical) axis
98139

99140
# The dtype.max is the sentinel value for the grid map.
100141
# Normally, this is uint32, but some grids need to be promoted to uint64.

src/mdio/segy/blocked_io.py

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import zarr
1313
from dask.array import Array
1414
from dask.array import map_blocks
15-
from segy import SegyFile
1615
from tqdm.auto import tqdm
1716
from zarr import open_group as zarr_open_group
1817

@@ -23,6 +22,7 @@
2322
from mdio.core.config import MDIOSettings
2423
from mdio.core.indexing import ChunkIterator
2524
from mdio.segy._workers import trace_worker
25+
from mdio.segy._workers import trace_worker_init
2626
from mdio.segy.creation import SegyPartRecord
2727
from mdio.segy.creation import concat_files
2828
from mdio.segy.creation import serialize_to_segy_stack
@@ -82,52 +82,44 @@ def to_zarr( # noqa: PLR0913, PLR0915
8282
num_chunks = chunk_iter.num_chunks
8383

8484
zarr_format = zarr.config.get("default_zarr_format")
85+
use_consolidated = zarr_format == ZarrFormat.V2
8586

86-
# Open zarr group once in main process
87+
# Open zarr group once in main process (used for final stats update below).
8788
storage_options = _normalize_storage_options(output_path)
8889
zarr_group = zarr_open_group(
8990
output_path.as_posix(),
9091
mode="r+",
9192
storage_options=storage_options,
92-
use_consolidated=zarr_format == ZarrFormat.V2,
93+
use_consolidated=use_consolidated,
9394
)
9495

95-
# Get array handles from the opened group
96-
data_array = zarr_group[data_variable_name]
97-
header_array = zarr_group.get("headers")
98-
raw_header_array = zarr_group.get("raw_headers")
99-
10096
# For Unix async writes with s3fs/fsspec & multiprocessing, use 'spawn' instead of default
10197
# 'fork' to avoid deadlocks on cloud stores. Slower but necessary. Default on Windows.
10298
num_workers = min(num_chunks, settings.import_cpus)
10399
context = mp.get_context("spawn")
104100

105-
# Use initializer to open segy file once per worker
101+
# Open the SEG-Y file, Zarr output handles, and transfer the compressed grid map once per worker
102+
# via the initializer. The grid map stays a compressed in-memory Zarr array and is sliced lazily
103+
# inside each worker, so we avoid both re-pickling per block and materializing the full dense map.
106104
executor = ProcessPoolExecutor(
107105
max_workers=num_workers,
108106
mp_context=context,
107+
initializer=trace_worker_init,
108+
initargs=(
109+
segy_file_kwargs,
110+
output_path.as_posix(),
111+
storage_options,
112+
use_consolidated,
113+
data_variable_name,
114+
grid_map,
115+
),
109116
)
110117

111-
segy_file = SegyFile(**segy_file_kwargs)
112-
113-
# Load in-memory Zarr grid map to NumPy array once to avoid Zarr slicing overhead in the submission loop
114-
grid_map_np = grid_map[:]
115-
116118
with executor:
117119
futures = []
118120
for region in chunk_iter:
119-
region_slices = tuple(region.values())
120-
local_grid_map = grid_map_np[region_slices[:-1]]
121-
# Pass zarr array handles and local grid map slice to workers
122-
future = executor.submit(
123-
trace_worker,
124-
segy_file,
125-
data_array,
126-
header_array,
127-
raw_header_array,
128-
region,
129-
local_grid_map,
130-
)
121+
# Only the lightweight region is pickled per block; shared inputs live in worker state.
122+
future = executor.submit(trace_worker, region)
131123
futures.append(future)
132124

133125
iterable = tqdm(

0 commit comments

Comments
 (0)