Skip to content

Commit 2af40c0

Browse files
authored
Merge pull request #1127 from xylar/fix-multiprocessing-for-python-3.14
Fix multiprocessing for python 3.14
2 parents e5cb4d3 + dbbe7ac commit 2af40c0

File tree

11 files changed

+148
-35
lines changed

11 files changed

+148
-35
lines changed

.github/workflows/build_workflow.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
shell: bash -l {0}
2828
strategy:
2929
matrix:
30-
python-version: ["3.10", "3.11", "3.12", "3.13"]
30+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
3131
fail-fast: false
3232
steps:
3333
- id: skip_check

.github/workflows/docs_workflow.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ on:
1010
types: [published]
1111

1212
env:
13-
PYTHON_VERSION: "3.13"
13+
PYTHON_VERSION: "3.14"
1414

1515
jobs:
1616
publish-docs:

ci/recipe/meta.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ requirements:
2323
- pip
2424
- setuptools >=60
2525
run:
26-
- python >={{ python_min }},<3.14
26+
- python >={{ python_min }},<3.13
2727
- cartopy >=0.18.0
2828
- cartopy_offlinedata
2929
- cmocean

dev-spec.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# $ conda create --name <env> --file <this file>
33

44
# Base
5-
python >=3.10,<3.14
5+
python >=3.10
66
cartopy >=0.18.0
77
cartopy_offlinedata
88
cmocean

mpas_analysis/__main__.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import time
3434
import json
3535
from importlib.metadata import Distribution
36-
from importlib.resources import contents
36+
from importlib.resources import files
3737

3838
from mache import discover_machine, MachineInfo
3939

@@ -1075,10 +1075,11 @@ def main():
10751075
except FileNotFoundError:
10761076

10771077
possible_machines = []
1078-
machine_configs = contents('mache.machines')
1079-
for config in machine_configs:
1080-
if config.endswith('.cfg'):
1081-
possible_machines.append(os.path.splitext(config)[0])
1078+
machine_configs = files('mache.machines').iterdir()
1079+
for config_file in machine_configs:
1080+
if config_file.name.endswith('.cfg'):
1081+
possible_machines.append(
1082+
os.path.splitext(config_file.name)[0])
10821083

10831084
possible_machines = '\n '.join(sorted(possible_machines))
10841085
raise ValueError(

mpas_analysis/ocean/climatology_map_ohc_anomaly.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ class RemapMpasOHCClimatology(RemapMpasClimatologySubtask):
182182
183183
min_depth, max_depth : float
184184
The minimum and maximum depths for integration
185+
186+
cp : float
187+
Specific heat of seawater [J/(kg*degC)]
188+
189+
rho : float
190+
Reference density of seawater [kg/m3]
185191
"""
186192

187193
def __init__(self, mpas_climatology_task, ref_year_climatology_task,
@@ -239,6 +245,8 @@ def __init__(self, mpas_climatology_task, ref_year_climatology_task,
239245
self.run_after(ref_year_climatology_task)
240246
self.min_depth = min_depth
241247
self.max_depth = max_depth
248+
self.cp = None
249+
self.rho = None
242250

243251
def setup_and_check(self):
244252
"""
@@ -255,6 +263,9 @@ def setup_and_check(self):
255263
self.ref_year_climatology_task.add_variables(self.variableList,
256264
self.seasons)
257265

266+
self.cp = self.namelist.getfloat('config_specific_heat_sea_water')
267+
self.rho = self.namelist.getfloat('config_density0')
268+
258269
def customize_masked_climatology(self, climatology, season):
259270
"""
260271
Compute the ocean heat content (OHC) anomaly from the temperature
@@ -298,10 +309,10 @@ def _compute_ohc(self, climatology):
298309
ds_mesh = xr.open_dataset(self.meshFilename)
299310
ds_mesh = ds_mesh.isel(Time=0)
300311

301-
# specific heat [J/(kg*degC)]
302-
cp = self.namelist.getfloat('config_specific_heat_sea_water')
303-
# [kg/m3]
304-
rho = self.namelist.getfloat('config_density0')
312+
cp = self.cp
313+
assert cp is not None, "Specific heat 'cp' has not been set"
314+
rho = self.rho
315+
assert rho is not None, "Reference density 'rho' has not been set"
305316

306317
units_scale_factor = 1e-9
307318

mpas_analysis/ocean/time_series_ohc_anomaly.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
class TimeSeriesOHCAnomaly(AnalysisTask):
3232
"""
3333
Performs analysis of ocean heat content (OHC) from time-series output.
34+
35+
Attributes
36+
----------
37+
cp : float
38+
Specific heat of seawater [J/(kg*degC)]
39+
40+
rho : float
41+
Reference density of seawater [kg/m3]
42+
43+
meshFilename : str
44+
The path to the MPAS mesh file
3445
"""
3546
# Authors
3647
# -------
@@ -132,17 +143,37 @@ def __init__(self, config, mpasTimeSeriesTask, controlConfig=None):
132143
plotTask.run_after(anomalyTask)
133144
self.add_subtask(plotTask)
134145

146+
self.cp = None
147+
self.rho = None
148+
self.meshFilename = None
149+
150+
def setup_and_check(self):
151+
"""
152+
Store the specific heat and reference density of seawater for use
153+
in OHC calculations.
154+
"""
155+
super().setup_and_check()
156+
157+
self.cp = self.namelist.getfloat('config_specific_heat_sea_water')
158+
self.rho = self.namelist.getfloat('config_density0')
159+
self.meshFilename = self.get_mesh_filename()
160+
135161
def _compute_ohc(self, ds):
136162
"""
137163
Compute the OHC time series.
138164
"""
139165
# for convenience, rename the variables to simpler, shorter names
140166
ds = ds.rename(self.variableDict)
141167

142-
# specific heat [J/(kg*degC)]
143-
cp = self.namelist.getfloat('config_specific_heat_sea_water')
144-
# [kg/m3]
145-
rho = self.namelist.getfloat('config_density0')
168+
# these need to be set at setup time, not at runtime because piclking
169+
# means the namelists and streams objects they come from aren't
170+
# available at runtime
171+
cp = self.cp
172+
assert cp is not None, "Specific heat 'cp' has not been set"
173+
rho = self.rho
174+
assert rho is not None, "Reference density 'rho' has not been set"
175+
meshFile = self.meshFilename
176+
assert meshFile is not None, "Mesh filename has not been set"
146177

147178
unitsScalefactor = 1e-22
148179

@@ -152,8 +183,6 @@ def _compute_ohc(self, ds):
152183
ds.ohc.attrs['units'] = '$10^{22}$ J'
153184
ds.ohc.attrs['description'] = 'Ocean heat content in each region'
154185

155-
meshFile = self.get_mesh_filename()
156-
157186
# Define/read in general variables
158187
with xr.open_dataset(meshFile) as dsMesh:
159188
# reference depth [m]

mpas_analysis/shared/analysis_task.py

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -203,18 +203,7 @@ def setup_and_check(self):
203203

204204
self.plotsDirectory = build_config_full_path(self.config, 'output',
205205
'plotsSubdirectory')
206-
namelistFileName = build_config_full_path(
207-
self.config, 'input',
208-
'{}NamelistFileName'.format(self.componentName))
209-
self.namelist = NameList(namelistFileName)
210-
211-
streamsFileName = build_config_full_path(
212-
self.config, 'input',
213-
'{}StreamsFileName'.format(self.componentName))
214-
self.runStreams = StreamsFile(streamsFileName,
215-
streamsdir=self.runDirectory)
216-
self.historyStreams = StreamsFile(streamsFileName,
217-
streamsdir=self.historyDirectory)
206+
self._load_namelists_and_streams()
218207

219208
self.calendar = self.namelist.get('config_calendar_type')
220209

@@ -282,6 +271,19 @@ def add_subtask(self, subtask):
282271
if subtask not in self.subtasks:
283272
self.subtasks.append(subtask)
284273

274+
def start(self):
275+
"""
276+
Clear unpicklable attributes and then start the analysis task as a new
277+
process.
278+
"""
279+
# Authors
280+
# -------
281+
# Xylar Asay-Davis
282+
# clear unpicklable attributes before running the task
283+
self._clear_namelists_and_streams()
284+
285+
super(AnalysisTask, self).start()
286+
285287
def run(self, writeLogFile=True):
286288
"""
287289
Sets up logging and then runs the analysis task.
@@ -320,6 +322,9 @@ def run(self, writeLogFile=True):
320322

321323
startTime = time.time()
322324
try:
325+
# reload namelists and streams, since they cannot be pickled
326+
# as part of multiprocessing
327+
self._load_namelists_and_streams()
323328
self.run_task()
324329
self._runStatus.value = AnalysisTask.SUCCESS
325330
except (Exception, BaseException) as e:
@@ -525,6 +530,71 @@ def get_mesh_filename(self):
525530

526531
return meshFilename
527532

533+
def __getstate__(self):
534+
"""
535+
Customize pickling to exclude unpicklable and unnecessary attributes.
536+
This method is called during multiprocessing when the task is
537+
serialized to be sent to a child process. We exclude task dependencies
538+
and process internals that don't need to be transferred, such as logger
539+
objects, process internals, and weakref-bearing attributes.
540+
541+
Returns
542+
-------
543+
state : dict
544+
The object state with unpicklable and unnecessary attributes
545+
removed.
546+
"""
547+
state = self.__dict__.copy()
548+
549+
# Clear out attributes that should not be pickled
550+
state['namelist'] = None
551+
state['runStreams'] = None
552+
state['historyStreams'] = None
553+
state['runAfterTasks'] = []
554+
state['subtasks'] = []
555+
# Drop process internals and logger that can't/shouldn't be pickled
556+
for key in ['_popen', 'logger', '_stackTrace']:
557+
state.pop(key, None)
558+
559+
# Drop weakref-bearing Finalize, etc., by not pickling _popen at all
560+
# _runStatus is a multiprocessing.Value; depending on your logic,
561+
# you may also want to skip it and let child initialize its own.
562+
563+
return state
564+
565+
def _load_namelists_and_streams(self):
566+
"""
567+
Load namelist and streams attributes.
568+
"""
569+
# Authors
570+
# -------
571+
# Xylar Asay-Davis
572+
573+
namelistFileName = build_config_full_path(
574+
self.config, 'input',
575+
'{}NamelistFileName'.format(self.componentName))
576+
self.namelist = NameList(namelistFileName)
577+
578+
streamsFileName = build_config_full_path(
579+
self.config, 'input',
580+
'{}StreamsFileName'.format(self.componentName))
581+
self.runStreams = StreamsFile(streamsFileName,
582+
streamsdir=self.runDirectory)
583+
self.historyStreams = StreamsFile(streamsFileName,
584+
streamsdir=self.historyDirectory)
585+
586+
def _clear_namelists_and_streams(self):
587+
"""
588+
Clear namelist and streams attributes that cannot be pickled for
589+
multiprocessing.
590+
"""
591+
# Authors
592+
# -------
593+
# Xylar Asay-Davis
594+
595+
self.namelist = None
596+
self.runStreams = None
597+
self.historyStreams = None
528598
# }}}
529599

530600

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@ description = """\
2727
"""
2828
license = { file = "LICENSE" }
2929
readme = "README.md"
30-
requires-python = ">=3.10,<3.14"
30+
requires-python = ">=3.10"
3131
classifiers = [
3232
# these are only for searching/browsing projects on PyPI
3333
"Programming Language :: Python :: 3.10",
3434
"Programming Language :: Python :: 3.11",
3535
"Programming Language :: Python :: 3.12",
3636
"Programming Language :: Python :: 3.13",
37+
"Programming Language :: Python :: 3.14",
3738

3839

3940
"Development Status :: 5 - Production/Stable",

suite/run_dev_suite.bash

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ cd ..
1919

2020
machine=$(python -c "from mache import discover_machine; print(discover_machine())")
2121

22-
py=3.13
22+
py=$(python -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")')
23+
2324
./suite/setup.py -p ${py} -r main_py${py} -b ${branch} --copy_docs --clean -e ${env_name}
2425
./suite/setup.py -p ${py} -r wc_defaults -b ${branch} --no_polar_regions -e ${env_name}
2526
./suite/setup.py -p ${py} -r moc_am -b ${branch} -e ${env_name}
@@ -33,7 +34,7 @@ py=3.13
3334
# submit the jobs
3435
cd ${machine}_test_suite
3536

36-
main_py=3.13
37+
main_py=${py}
3738
cd main_py${main_py}
3839
echo main_py${main_py}
3940
RES=$(sbatch job_script.bash)

0 commit comments

Comments
 (0)