vortex-nwp 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vortex/__init__.py +159 -0
- vortex/algo/__init__.py +13 -0
- vortex/algo/components.py +2462 -0
- vortex/algo/mpitools.py +1953 -0
- vortex/algo/mpitools_templates/__init__.py +1 -0
- vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
- vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
- vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
- vortex/algo/serversynctools.py +171 -0
- vortex/config.py +112 -0
- vortex/data/__init__.py +19 -0
- vortex/data/abstractstores.py +1510 -0
- vortex/data/containers.py +835 -0
- vortex/data/contents.py +622 -0
- vortex/data/executables.py +275 -0
- vortex/data/flow.py +119 -0
- vortex/data/geometries.ini +2689 -0
- vortex/data/geometries.py +799 -0
- vortex/data/handlers.py +1230 -0
- vortex/data/outflow.py +67 -0
- vortex/data/providers.py +487 -0
- vortex/data/resources.py +207 -0
- vortex/data/stores.py +1390 -0
- vortex/data/sync_templates/__init__.py +0 -0
- vortex/gloves.py +309 -0
- vortex/layout/__init__.py +20 -0
- vortex/layout/contexts.py +577 -0
- vortex/layout/dataflow.py +1220 -0
- vortex/layout/monitor.py +969 -0
- vortex/nwp/__init__.py +14 -0
- vortex/nwp/algo/__init__.py +21 -0
- vortex/nwp/algo/assim.py +537 -0
- vortex/nwp/algo/clim.py +1086 -0
- vortex/nwp/algo/coupling.py +831 -0
- vortex/nwp/algo/eda.py +840 -0
- vortex/nwp/algo/eps.py +785 -0
- vortex/nwp/algo/forecasts.py +886 -0
- vortex/nwp/algo/fpserver.py +1303 -0
- vortex/nwp/algo/ifsnaming.py +463 -0
- vortex/nwp/algo/ifsroot.py +404 -0
- vortex/nwp/algo/monitoring.py +263 -0
- vortex/nwp/algo/mpitools.py +694 -0
- vortex/nwp/algo/odbtools.py +1258 -0
- vortex/nwp/algo/oopsroot.py +916 -0
- vortex/nwp/algo/oopstests.py +220 -0
- vortex/nwp/algo/request.py +660 -0
- vortex/nwp/algo/stdpost.py +1641 -0
- vortex/nwp/data/__init__.py +30 -0
- vortex/nwp/data/assim.py +380 -0
- vortex/nwp/data/boundaries.py +314 -0
- vortex/nwp/data/climfiles.py +521 -0
- vortex/nwp/data/configfiles.py +153 -0
- vortex/nwp/data/consts.py +954 -0
- vortex/nwp/data/ctpini.py +149 -0
- vortex/nwp/data/diagnostics.py +209 -0
- vortex/nwp/data/eda.py +147 -0
- vortex/nwp/data/eps.py +432 -0
- vortex/nwp/data/executables.py +1045 -0
- vortex/nwp/data/fields.py +111 -0
- vortex/nwp/data/gridfiles.py +380 -0
- vortex/nwp/data/logs.py +584 -0
- vortex/nwp/data/modelstates.py +363 -0
- vortex/nwp/data/monitoring.py +193 -0
- vortex/nwp/data/namelists.py +696 -0
- vortex/nwp/data/obs.py +840 -0
- vortex/nwp/data/oopsexec.py +74 -0
- vortex/nwp/data/providers.py +207 -0
- vortex/nwp/data/query.py +206 -0
- vortex/nwp/data/stores.py +160 -0
- vortex/nwp/data/surfex.py +337 -0
- vortex/nwp/syntax/__init__.py +9 -0
- vortex/nwp/syntax/stdattrs.py +437 -0
- vortex/nwp/tools/__init__.py +10 -0
- vortex/nwp/tools/addons.py +40 -0
- vortex/nwp/tools/agt.py +67 -0
- vortex/nwp/tools/bdap.py +59 -0
- vortex/nwp/tools/bdcp.py +41 -0
- vortex/nwp/tools/bdm.py +24 -0
- vortex/nwp/tools/bdmp.py +54 -0
- vortex/nwp/tools/conftools.py +1661 -0
- vortex/nwp/tools/drhook.py +66 -0
- vortex/nwp/tools/grib.py +294 -0
- vortex/nwp/tools/gribdiff.py +104 -0
- vortex/nwp/tools/ifstools.py +203 -0
- vortex/nwp/tools/igastuff.py +273 -0
- vortex/nwp/tools/mars.py +68 -0
- vortex/nwp/tools/odb.py +657 -0
- vortex/nwp/tools/partitioning.py +258 -0
- vortex/nwp/tools/satrad.py +71 -0
- vortex/nwp/util/__init__.py +6 -0
- vortex/nwp/util/async.py +212 -0
- vortex/nwp/util/beacon.py +40 -0
- vortex/nwp/util/diffpygram.py +447 -0
- vortex/nwp/util/ens.py +279 -0
- vortex/nwp/util/hooks.py +139 -0
- vortex/nwp/util/taskdeco.py +85 -0
- vortex/nwp/util/usepygram.py +697 -0
- vortex/nwp/util/usetnt.py +101 -0
- vortex/proxy.py +6 -0
- vortex/sessions.py +374 -0
- vortex/syntax/__init__.py +9 -0
- vortex/syntax/stdattrs.py +867 -0
- vortex/syntax/stddeco.py +185 -0
- vortex/toolbox.py +1117 -0
- vortex/tools/__init__.py +20 -0
- vortex/tools/actions.py +523 -0
- vortex/tools/addons.py +316 -0
- vortex/tools/arm.py +96 -0
- vortex/tools/compression.py +325 -0
- vortex/tools/date.py +27 -0
- vortex/tools/ddhpack.py +10 -0
- vortex/tools/delayedactions.py +782 -0
- vortex/tools/env.py +541 -0
- vortex/tools/folder.py +834 -0
- vortex/tools/grib.py +738 -0
- vortex/tools/lfi.py +953 -0
- vortex/tools/listings.py +423 -0
- vortex/tools/names.py +637 -0
- vortex/tools/net.py +2124 -0
- vortex/tools/odb.py +10 -0
- vortex/tools/parallelism.py +368 -0
- vortex/tools/prestaging.py +210 -0
- vortex/tools/rawfiles.py +10 -0
- vortex/tools/schedulers.py +480 -0
- vortex/tools/services.py +940 -0
- vortex/tools/storage.py +996 -0
- vortex/tools/surfex.py +61 -0
- vortex/tools/systems.py +3976 -0
- vortex/tools/targets.py +440 -0
- vortex/util/__init__.py +9 -0
- vortex/util/config.py +1122 -0
- vortex/util/empty.py +24 -0
- vortex/util/helpers.py +216 -0
- vortex/util/introspection.py +69 -0
- vortex/util/iosponge.py +80 -0
- vortex/util/roles.py +49 -0
- vortex/util/storefunctions.py +129 -0
- vortex/util/structs.py +26 -0
- vortex/util/worker.py +162 -0
- vortex_nwp-2.0.0.dist-info/METADATA +67 -0
- vortex_nwp-2.0.0.dist-info/RECORD +144 -0
- vortex_nwp-2.0.0.dist-info/WHEEL +5 -0
- vortex_nwp-2.0.0.dist-info/licenses/LICENSE +517 -0
- vortex_nwp-2.0.0.dist-info/top_level.txt +1 -0
vortex/algo/mpitools.py
ADDED
|
@@ -0,0 +1,1953 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This package handles MPI interface objects responsible of parallel executions.
|
|
3
|
+
:class:`MpiTool` and :class:`MpiBinaryDescription` objects use the
|
|
4
|
+
:mod:`footprints` mechanism.
|
|
5
|
+
|
|
6
|
+
A :class:`MpiTool` object is directly related to a concrete MPI implementation: it
|
|
7
|
+
builds the proper command line, update the namelists with relevant MPI parameters
|
|
8
|
+
(for instance, the total number of tasks), update the environment to fit the MPI
|
|
9
|
+
implementation needs, ... It heavily relies on :class:`MpiBinaryDescription`
|
|
10
|
+
objects that describe the settings and behaviours associated with each of the
|
|
11
|
+
binaries that will be launched.
|
|
12
|
+
|
|
13
|
+
Here is a typical use of MpiTools:
|
|
14
|
+
|
|
15
|
+
.. code-block:: python
|
|
16
|
+
|
|
17
|
+
# We will assume that bin0, bin1 are valid executable's Resource Handlers
|
|
18
|
+
|
|
19
|
+
from footprints import proxy as fpx
|
|
20
|
+
import vortex
|
|
21
|
+
|
|
22
|
+
t = vortex.ticket()
|
|
23
|
+
|
|
24
|
+
# Create the mpitool object for a given MPI implementation
|
|
25
|
+
mpitool = fpx.mpitool(sysname=t.system().sysname,
|
|
26
|
+
mpiname='mpirun', # To use Open-MPI's mpirun
|
|
27
|
+
)
|
|
28
|
+
# NB: mpiname='...' may be omitted. In such a case, the VORTEX_MPI_NAME
|
|
29
|
+
# environment variable is used
|
|
30
|
+
|
|
31
|
+
# Create the MPI binaires descriptions
|
|
32
|
+
dbin0 = fpx.mpibinary(kind='basic', nodes=2, tasks=4, openmp=10)
|
|
33
|
+
dbin0.master = bin0.container.localpath()
|
|
34
|
+
dbin1 = fpx.mpibinary(kind='basic', nodes=1, tasks=8, openmp=5)
|
|
35
|
+
dbin1.master = bin1.container.localpath()
|
|
36
|
+
|
|
37
|
+
# Note: the number of nodes, tasks, ... can be overwritten at any time using:
|
|
38
|
+
# dbinX.options = dict(nn=M, nnp=N, openmp=P)
|
|
39
|
+
|
|
40
|
+
# Associate the MPI binaires descriptions to the mpitool object
|
|
41
|
+
mpitool.binaries = [dbin0, dbin1]
|
|
42
|
+
|
|
43
|
+
bargs = ['-test bin0' # Command line arguments for bin0
|
|
44
|
+
'-test bin1' ] # Command line arguments for bin1
|
|
45
|
+
# Build the MPI command line :
|
|
46
|
+
args = mpitool.mkcmdline(bargs)
|
|
47
|
+
|
|
48
|
+
# Setup various usefull things (env, system, ...)
|
|
49
|
+
mpitool.import_basics(an_algo_component_object)
|
|
50
|
+
|
|
51
|
+
# Specific parallel settings (the namelists and environment may be modified here)
|
|
52
|
+
mpitool.setup(dict()) # The dictionary may contain additional options
|
|
53
|
+
|
|
54
|
+
# ...
|
|
55
|
+
# Here you may run the command contained in *args*
|
|
56
|
+
# ...
|
|
57
|
+
|
|
58
|
+
# Specific parallel cleaning
|
|
59
|
+
mpitool.clean(opts)
|
|
60
|
+
|
|
61
|
+
Actually, in real scripts, all of this is carried out by the
|
|
62
|
+
:class:`vortex.algo.components.Parallel` class which saves a lot of hassle.
|
|
63
|
+
|
|
64
|
+
Note: Namelists and environment changes are orchestrated as follows:
|
|
65
|
+
* Changes (if any) are apply be the :class:`MpiTool` object
|
|
66
|
+
* Changes (if any) are apply by each of the :class:`MpiBinaryDescription` objects
|
|
67
|
+
attached to the MpiTool object
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
import collections
|
|
72
|
+
import collections.abc
|
|
73
|
+
import importlib
|
|
74
|
+
import itertools
|
|
75
|
+
import locale
|
|
76
|
+
import re
|
|
77
|
+
import shlex
|
|
78
|
+
|
|
79
|
+
import footprints
|
|
80
|
+
from bronx.fancies import loggers
|
|
81
|
+
from bronx.syntax.parsing import xlist_strings
|
|
82
|
+
from vortex.config import from_config
|
|
83
|
+
from vortex.tools import env
|
|
84
|
+
from vortex.tools.arm import ArmForgeTool
|
|
85
|
+
from vortex.tools.systems import ExecutionError
|
|
86
|
+
from vortex.util import config
|
|
87
|
+
|
|
88
|
+
#: No automatic export
|
|
89
|
+
__all__ = []
|
|
90
|
+
|
|
91
|
+
logger = loggers.getLogger(__name__)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class MpiException(Exception):
|
|
95
|
+
"""Raise an exception in the parallel execution mode."""
|
|
96
|
+
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class MpiTool(footprints.FootprintBase):
|
|
101
|
+
"""Root class for any :class:`MpiTool` subclass."""
|
|
102
|
+
|
|
103
|
+
_abstract = True
|
|
104
|
+
_collector = ("mpitool",)
|
|
105
|
+
_footprint = dict(
|
|
106
|
+
info="MpiTool class in charge of a particular MPI implementation",
|
|
107
|
+
attr=dict(
|
|
108
|
+
sysname=dict(
|
|
109
|
+
info="The current OS name (e.g. Linux)",
|
|
110
|
+
),
|
|
111
|
+
mpiname=dict(
|
|
112
|
+
info="The MPI implementation one wishes to use",
|
|
113
|
+
),
|
|
114
|
+
mpilauncher=dict(
|
|
115
|
+
info="The MPI launcher command to be used", optional=True
|
|
116
|
+
),
|
|
117
|
+
mpiopts=dict(
|
|
118
|
+
info="Extra arguments for the MPI command",
|
|
119
|
+
optional=True,
|
|
120
|
+
default="",
|
|
121
|
+
),
|
|
122
|
+
mpiwrapstd=dict(
|
|
123
|
+
info="When using the Vortex' global wrapper redirect stderr/stdout",
|
|
124
|
+
type=bool,
|
|
125
|
+
optional=True,
|
|
126
|
+
default=False,
|
|
127
|
+
doc_visibility=footprints.doc.visibility.ADVANCED,
|
|
128
|
+
doc_zorder=-90,
|
|
129
|
+
),
|
|
130
|
+
mpibind_topology=dict(
|
|
131
|
+
optional=True,
|
|
132
|
+
default="numapacked",
|
|
133
|
+
doc_visibility=footprints.doc.visibility.ADVANCED,
|
|
134
|
+
doc_zorder=-90,
|
|
135
|
+
),
|
|
136
|
+
optsep=dict(
|
|
137
|
+
info="Separator between MPI options and the program name",
|
|
138
|
+
optional=True,
|
|
139
|
+
default="--",
|
|
140
|
+
),
|
|
141
|
+
optprefix=dict(
|
|
142
|
+
info="MPI options prefix", optional=True, default="--"
|
|
143
|
+
),
|
|
144
|
+
optmap=dict(
|
|
145
|
+
info=(
|
|
146
|
+
"Mapping between MpiBinaryDescription objects "
|
|
147
|
+
+ "internal data and actual command line options"
|
|
148
|
+
),
|
|
149
|
+
type=footprints.FPDict,
|
|
150
|
+
optional=True,
|
|
151
|
+
default=footprints.FPDict(nn="nn", nnp="nnp", openmp="openmp"),
|
|
152
|
+
),
|
|
153
|
+
binsep=dict(
|
|
154
|
+
info="Separator between multiple binary groups",
|
|
155
|
+
optional=True,
|
|
156
|
+
default="--",
|
|
157
|
+
),
|
|
158
|
+
basics=dict(
|
|
159
|
+
type=footprints.FPList,
|
|
160
|
+
optional=True,
|
|
161
|
+
default=footprints.FPList(
|
|
162
|
+
[
|
|
163
|
+
"system",
|
|
164
|
+
"env",
|
|
165
|
+
"target",
|
|
166
|
+
"context",
|
|
167
|
+
"ticket",
|
|
168
|
+
]
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
bindingmethod=dict(
|
|
172
|
+
info="How to bind the MPI processes",
|
|
173
|
+
values=[
|
|
174
|
+
"vortex",
|
|
175
|
+
],
|
|
176
|
+
access="rwx",
|
|
177
|
+
optional=True,
|
|
178
|
+
doc_visibility=footprints.doc.visibility.ADVANCED,
|
|
179
|
+
doc_zorder=-90,
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
_envelope_bit_kind = "basicenvelopebit"
|
|
185
|
+
_envelope_wrapper_tpl = "envelope_wrapper_default.tpl"
|
|
186
|
+
_wrapstd_wrapper_tpl = "wrapstd_wrapper_default.tpl"
|
|
187
|
+
_envelope_wrapper_name = "./global_envelope_wrapper.py"
|
|
188
|
+
_wrapstd_wrapper_name = "./global_wrapstd_wrapper.py"
|
|
189
|
+
_envelope_rank_var = "MPIRANK"
|
|
190
|
+
_supports_manual_ranks_mapping = False
|
|
191
|
+
_needs_mpilib_specific_mpienv = True
|
|
192
|
+
|
|
193
|
+
def __init__(self, *args, **kw):
|
|
194
|
+
"""After parent initialization, set master, options and basics to undefined."""
|
|
195
|
+
logger.debug("Abstract mpi tool init %s", self.__class__)
|
|
196
|
+
super().__init__(*args, **kw)
|
|
197
|
+
self._launcher = self.mpilauncher or self.generic_mpiname
|
|
198
|
+
self._binaries = []
|
|
199
|
+
self._envelope = []
|
|
200
|
+
self._sources = []
|
|
201
|
+
self._mpilib_data_cache = None
|
|
202
|
+
self._mpilib_identification_cache = None
|
|
203
|
+
self._ranks_map_cache = None
|
|
204
|
+
self._complex_ranks_map = None
|
|
205
|
+
for k in self.basics:
|
|
206
|
+
self.__dict__["_" + k] = None
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def realkind(self):
|
|
210
|
+
return "mpitool"
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def generic_mpiname(self):
|
|
214
|
+
return self.mpiname.split("-")[0]
|
|
215
|
+
|
|
216
|
+
def __getattr__(self, key):
|
|
217
|
+
"""Have a look to basics values provided by some proxy."""
|
|
218
|
+
if key in self.basics:
|
|
219
|
+
return getattr(self, "_" + key)
|
|
220
|
+
else:
|
|
221
|
+
raise AttributeError(
|
|
222
|
+
"Attribute [%s] is not a basic mpitool attribute" % key
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def import_basics(self, obj, attrs=None):
|
|
226
|
+
"""Import some current values such as system, env, target and context from provided ``obj``."""
|
|
227
|
+
if attrs is None:
|
|
228
|
+
attrs = self.basics
|
|
229
|
+
for k in [x for x in attrs if x in self.basics and hasattr(obj, x)]:
|
|
230
|
+
setattr(self, "_" + k, getattr(obj, k))
|
|
231
|
+
for bin_obj in self.binaries:
|
|
232
|
+
bin_obj.import_basics(obj, attrs=None)
|
|
233
|
+
|
|
234
|
+
def _get_launcher(self):
|
|
235
|
+
"""
|
|
236
|
+
Returns the name of the mpi tool to be used, set from VORTEX_MPI_LAUNCHER
|
|
237
|
+
environment variable, current attribute :attr:`mpiname` or explicit setting.
|
|
238
|
+
"""
|
|
239
|
+
return self._launcher
|
|
240
|
+
|
|
241
|
+
def _set_launcher(self, value):
|
|
242
|
+
"""Set current launcher mpi name. Should be some special trick, so issue a warning."""
|
|
243
|
+
logger.warning(
|
|
244
|
+
"Setting a new value [%s] to mpi launcher [%s].", value, self
|
|
245
|
+
)
|
|
246
|
+
self._launcher = value
|
|
247
|
+
|
|
248
|
+
launcher = property(_get_launcher, _set_launcher)
|
|
249
|
+
|
|
250
|
+
def _get_envelope(self):
|
|
251
|
+
"""Returns the envelope description."""
|
|
252
|
+
return self._envelope
|
|
253
|
+
|
|
254
|
+
def _valid_envelope(self, value):
|
|
255
|
+
"""Tweak the envelope description values."""
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
def _set_envelope(self, value):
|
|
259
|
+
"""Set the envelope description."""
|
|
260
|
+
if not (
|
|
261
|
+
isinstance(value, collections.abc.Iterable)
|
|
262
|
+
and all(
|
|
263
|
+
[
|
|
264
|
+
isinstance(b, dict)
|
|
265
|
+
and all(
|
|
266
|
+
[
|
|
267
|
+
bk in ("nn", "nnp", "openmp", "np")
|
|
268
|
+
for bk in b.keys()
|
|
269
|
+
]
|
|
270
|
+
)
|
|
271
|
+
for b in value
|
|
272
|
+
]
|
|
273
|
+
)
|
|
274
|
+
):
|
|
275
|
+
raise ValueError("This should be an Iterable of dictionaries.")
|
|
276
|
+
self._valid_envelope(value)
|
|
277
|
+
self._envelope = list()
|
|
278
|
+
for e in value:
|
|
279
|
+
e_bit = footprints.proxy.mpibinary(kind=self._envelope_bit_kind)
|
|
280
|
+
self._envelope_fix_envelope_bit(e_bit, e)
|
|
281
|
+
self._envelope.append(e_bit)
|
|
282
|
+
|
|
283
|
+
envelope = property(_get_envelope, _set_envelope)
|
|
284
|
+
|
|
285
|
+
def _get_binaries(self):
|
|
286
|
+
"""Returns the list of :class:`MpiBinaryDescription` objects associated with this instance."""
|
|
287
|
+
return self._binaries
|
|
288
|
+
|
|
289
|
+
def _set_envelope_from_binaries(self):
|
|
290
|
+
"""Create an envelope from existing binaries."""
|
|
291
|
+
# Detect possible groups of binaries
|
|
292
|
+
groups = collections.defaultdict(list)
|
|
293
|
+
for a_bin in self.binaries:
|
|
294
|
+
if a_bin.group is not None:
|
|
295
|
+
groups[a_bin.group].append(a_bin)
|
|
296
|
+
new_envelope = list()
|
|
297
|
+
for a_bin in self.binaries:
|
|
298
|
+
if a_bin.group is None:
|
|
299
|
+
# The usual (and easy) case
|
|
300
|
+
new_envelope.append(
|
|
301
|
+
{
|
|
302
|
+
k: v
|
|
303
|
+
for k, v in a_bin.options.items()
|
|
304
|
+
if k in ("nn", "nnp", "openmp", "np")
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
elif a_bin.group in groups:
|
|
308
|
+
# Deal with group of binaries
|
|
309
|
+
group = groups.pop(a_bin.group)
|
|
310
|
+
n_nodes = {g_bin.options.get("nn", None) for g_bin in group}
|
|
311
|
+
if None in n_nodes:
|
|
312
|
+
raise ValueError(
|
|
313
|
+
"To build a proper envelope, "
|
|
314
|
+
+ '"nn" needs to be specified in all binaries'
|
|
315
|
+
)
|
|
316
|
+
done_nodes = 0
|
|
317
|
+
for n_node in sorted(n_nodes):
|
|
318
|
+
new_desc = {}
|
|
319
|
+
new_desc["nn"] = n_node - done_nodes
|
|
320
|
+
new_desc["nnp"] = 0
|
|
321
|
+
for g_bin in [
|
|
322
|
+
g_bin
|
|
323
|
+
for g_bin in group
|
|
324
|
+
if g_bin.options["nn"] >= n_node
|
|
325
|
+
]:
|
|
326
|
+
new_desc["nnp"] += g_bin.options["nnp"]
|
|
327
|
+
new_envelope.append(new_desc)
|
|
328
|
+
done_nodes = n_node
|
|
329
|
+
self.envelope = new_envelope
|
|
330
|
+
|
|
331
|
+
def _set_binaries_hack(self, binaries):
|
|
332
|
+
"""Perform any action right after the binaries have been setup."""
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
def _set_binaries_envelope_hack(self, binaries):
|
|
336
|
+
"""Tweak the envelope after binaries were setup."""
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
def _set_binaries(self, value):
|
|
340
|
+
"""Set the list of :class:`MpiBinaryDescription` objects associated with this instance."""
|
|
341
|
+
if not (
|
|
342
|
+
isinstance(value, collections.abc.Iterable)
|
|
343
|
+
and all([isinstance(b, MpiBinary) for b in value])
|
|
344
|
+
):
|
|
345
|
+
raise ValueError(
|
|
346
|
+
"This should be an Iterable of MpiBinary instances."
|
|
347
|
+
)
|
|
348
|
+
has_bin_groups = not all([b.group is None for b in value])
|
|
349
|
+
if not (self._supports_manual_ranks_mapping or not has_bin_groups):
|
|
350
|
+
raise ValueError(
|
|
351
|
+
"Binary groups are not supported by this MpiTool class"
|
|
352
|
+
)
|
|
353
|
+
has_bin_distribution = not all([b.distribution is None for b in value])
|
|
354
|
+
if not (
|
|
355
|
+
self._supports_manual_ranks_mapping or not has_bin_distribution
|
|
356
|
+
):
|
|
357
|
+
raise ValueError(
|
|
358
|
+
"Binary distribution option is not supported by this MpiTool class"
|
|
359
|
+
)
|
|
360
|
+
self._binaries = value
|
|
361
|
+
if not self.envelope and self.bindingmethod == "vortex":
|
|
362
|
+
self._set_envelope_from_binaries()
|
|
363
|
+
elif not self.envelope and (has_bin_groups or has_bin_distribution):
|
|
364
|
+
self._set_envelope_from_binaries()
|
|
365
|
+
self._set_binaries_hack(self._binaries)
|
|
366
|
+
if self.envelope:
|
|
367
|
+
self._set_binaries_envelope_hack(self._binaries)
|
|
368
|
+
self._mpilib_data_cache = None
|
|
369
|
+
self._mpilib_identification_cache = None
|
|
370
|
+
self._ranks_map_cache = None
|
|
371
|
+
self._complex_ranks_map = None
|
|
372
|
+
|
|
373
|
+
binaries = property(_get_binaries, _set_binaries)
|
|
374
|
+
|
|
375
|
+
def _mpilib_data(self):
|
|
376
|
+
"""From the binaries, try to detect MPI library and mpirun paths."""
|
|
377
|
+
if self._mpilib_data_cache is None:
|
|
378
|
+
mpilib_guesses = (
|
|
379
|
+
"libmpi.so",
|
|
380
|
+
"libmpi_mt.so",
|
|
381
|
+
"libmpi_dbg.so",
|
|
382
|
+
"libmpi_dbg_mt.so",
|
|
383
|
+
)
|
|
384
|
+
shp = self.system.path
|
|
385
|
+
mpilib_data = set()
|
|
386
|
+
for binary in self.binaries:
|
|
387
|
+
# For each binary call ldd...
|
|
388
|
+
mpilib = None
|
|
389
|
+
try:
|
|
390
|
+
binlibs = self.system.ldd(binary.master)
|
|
391
|
+
except (RuntimeError, ValueError):
|
|
392
|
+
# May fail if the 'master' is not a binary
|
|
393
|
+
continue
|
|
394
|
+
for mpilib_guess in mpilib_guesses:
|
|
395
|
+
for l, lp in binlibs.items():
|
|
396
|
+
if l.startswith(mpilib_guess):
|
|
397
|
+
mpilib = lp
|
|
398
|
+
break
|
|
399
|
+
if mpilib:
|
|
400
|
+
break
|
|
401
|
+
if mpilib:
|
|
402
|
+
mpilib = shp.normpath(mpilib)
|
|
403
|
+
mpitoolsdir = None
|
|
404
|
+
mpidir = shp.dirname(shp.dirname(mpilib))
|
|
405
|
+
if shp.exists(shp.join(mpidir, "bin", "mpirun")):
|
|
406
|
+
mpitoolsdir = shp.join(mpidir, "bin")
|
|
407
|
+
if not mpitoolsdir and shp.exists(
|
|
408
|
+
shp.join(mpidir, "..", "bin", "mpirun")
|
|
409
|
+
):
|
|
410
|
+
mpitoolsdir = shp.normpath(
|
|
411
|
+
shp.join(mpidir, "..", "bin")
|
|
412
|
+
)
|
|
413
|
+
if mpilib and mpitoolsdir:
|
|
414
|
+
mpilib_data.add(
|
|
415
|
+
(shp.realpath(mpilib), shp.realpath(mpitoolsdir))
|
|
416
|
+
)
|
|
417
|
+
# All the binary must use the same library !
|
|
418
|
+
if len(mpilib_data) == 0:
|
|
419
|
+
logger.info("No MPI library was detected.")
|
|
420
|
+
self._mpilib_data_cache = ()
|
|
421
|
+
elif len(mpilib_data) > 1:
|
|
422
|
+
logger.error("Multiple MPI library were detected.")
|
|
423
|
+
self._mpilib_data_cache = ()
|
|
424
|
+
else:
|
|
425
|
+
self._mpilib_data_cache = mpilib_data.pop()
|
|
426
|
+
return self._mpilib_data_cache if self._mpilib_data_cache else None
|
|
427
|
+
|
|
428
|
+
def _mpilib_match_result(self, regex, rclines, which):
|
|
429
|
+
for line in rclines:
|
|
430
|
+
matched = regex.match(line)
|
|
431
|
+
if matched:
|
|
432
|
+
logger.info(
|
|
433
|
+
"MPI implementation detected: %s (%s)",
|
|
434
|
+
which,
|
|
435
|
+
" ".join(matched.groups()),
|
|
436
|
+
)
|
|
437
|
+
return [which] + [int(res) for res in matched.groups()]
|
|
438
|
+
return False
|
|
439
|
+
|
|
440
|
+
def _mpilib_identification(self):
|
|
441
|
+
"""Try to guess the name and version of the MPI library."""
|
|
442
|
+
if self._mpilib_data() is None:
|
|
443
|
+
return None
|
|
444
|
+
if self._mpilib_identification_cache is None:
|
|
445
|
+
mpi_lib, mpi_tools_dir = self._mpilib_data()
|
|
446
|
+
ld_libs_extra = set()
|
|
447
|
+
sh = self.system
|
|
448
|
+
mpirun_path = sh.path.join(mpi_tools_dir, "mpirun")
|
|
449
|
+
if sh.path.exists(mpirun_path):
|
|
450
|
+
try:
|
|
451
|
+
libs = sh.ldd(mpirun_path)
|
|
452
|
+
except ExecutionError:
|
|
453
|
+
# This may happen if the mpirun binary is statically linked
|
|
454
|
+
libs = dict()
|
|
455
|
+
if any([libname is None for libname in libs.values()]):
|
|
456
|
+
libscache = dict()
|
|
457
|
+
for binary in self.binaries:
|
|
458
|
+
for lib, libpath in sh.ldd(binary.master).items():
|
|
459
|
+
if libpath:
|
|
460
|
+
libscache[lib] = sh.path.dirname(libpath)
|
|
461
|
+
for missing_lib in [
|
|
462
|
+
lib for lib, libname in libs.items() if libname is None
|
|
463
|
+
]:
|
|
464
|
+
if missing_lib in libscache:
|
|
465
|
+
ld_libs_extra.add(libscache[missing_lib])
|
|
466
|
+
with self.env.clone() as localenv:
|
|
467
|
+
for libpath in ld_libs_extra:
|
|
468
|
+
localenv.setgenericpath("LD_LIBRARY_PATH", libpath)
|
|
469
|
+
rc = sh.spawn(
|
|
470
|
+
[mpirun_path, "--version"], output=True, fatal=False
|
|
471
|
+
)
|
|
472
|
+
if rc:
|
|
473
|
+
id_res = self._mpilib_match_result(
|
|
474
|
+
re.compile(
|
|
475
|
+
r"^.*Intel.*MPI.*Version\s+(\d+)\s+Update\s+(\d+)",
|
|
476
|
+
re.IGNORECASE,
|
|
477
|
+
),
|
|
478
|
+
rc,
|
|
479
|
+
"intelmpi",
|
|
480
|
+
)
|
|
481
|
+
id_res = id_res or self._mpilib_match_result(
|
|
482
|
+
re.compile(
|
|
483
|
+
r"^.*Open\s*MPI.*\s+(\d+)\.(\d+)(?:\.(\d+))?",
|
|
484
|
+
re.IGNORECASE,
|
|
485
|
+
),
|
|
486
|
+
rc,
|
|
487
|
+
"openmpi",
|
|
488
|
+
)
|
|
489
|
+
if id_res:
|
|
490
|
+
ld_libs_extra = tuple(sorted(ld_libs_extra))
|
|
491
|
+
self._mpilib_identification_cache = tuple(
|
|
492
|
+
[mpi_lib, mpi_tools_dir, ld_libs_extra] + id_res
|
|
493
|
+
)
|
|
494
|
+
if self._mpilib_identification_cache is None:
|
|
495
|
+
ld_libs_extra = tuple(sorted(ld_libs_extra))
|
|
496
|
+
self._mpilib_identification_cache = (
|
|
497
|
+
mpi_lib,
|
|
498
|
+
mpi_tools_dir,
|
|
499
|
+
ld_libs_extra,
|
|
500
|
+
"unknown",
|
|
501
|
+
)
|
|
502
|
+
return self._mpilib_identification_cache
|
|
503
|
+
|
|
504
|
+
def _get_sources(self):
|
|
505
|
+
"""Returns a list of directories that may contain source files."""
|
|
506
|
+
return self._sources
|
|
507
|
+
|
|
508
|
+
def _set_sources(self, value):
|
|
509
|
+
"""Set the list of of directories that may contain source files."""
|
|
510
|
+
if not isinstance(value, collections.abc.Iterable):
|
|
511
|
+
raise ValueError("This should be an Iterable.")
|
|
512
|
+
self._sources = value
|
|
513
|
+
|
|
514
|
+
sources = property(_get_sources, _set_sources)
|
|
515
|
+
|
|
516
|
+
def _actual_mpiopts(self):
|
|
517
|
+
"""The mpiopts string."""
|
|
518
|
+
return self.mpiopts
|
|
519
|
+
|
|
520
|
+
def _reshaped_mpiopts(self):
|
|
521
|
+
"""Raw list of mpi tool command line options."""
|
|
522
|
+
klast = None
|
|
523
|
+
options = collections.defaultdict(list)
|
|
524
|
+
for optdef in shlex.split(self._actual_mpiopts()):
|
|
525
|
+
if optdef.startswith("-"):
|
|
526
|
+
optdef = optdef.lstrip("-")
|
|
527
|
+
options[optdef].append([])
|
|
528
|
+
klast = optdef
|
|
529
|
+
elif klast is not None:
|
|
530
|
+
options[klast][-1].append(optdef)
|
|
531
|
+
else:
|
|
532
|
+
raise MpiException(
|
|
533
|
+
"Badly shaped mpi option around {!s}".format(optdef)
|
|
534
|
+
)
|
|
535
|
+
return options
|
|
536
|
+
|
|
537
|
+
def _hook_binary_mpiopts(self, binary, options):
|
|
538
|
+
"""A nasty hook to modify binaries' mpiopts on the fly."""
|
|
539
|
+
return options
|
|
540
|
+
|
|
541
|
+
@property
|
|
542
|
+
def _ranks_mapping(self):
|
|
543
|
+
"""When group are defined, associate each MPI rank with a "real" slot."""
|
|
544
|
+
if self._ranks_map_cache is None:
|
|
545
|
+
self._complex_ranks_map = False
|
|
546
|
+
if not self.envelope:
|
|
547
|
+
raise RuntimeError(
|
|
548
|
+
"Ranks mapping should always be used within an envelope."
|
|
549
|
+
)
|
|
550
|
+
# First deal with bingroups
|
|
551
|
+
ranks_map = dict()
|
|
552
|
+
has_bin_groups = not all([b.group is None for b in self.binaries])
|
|
553
|
+
cursor = 0 # The MPI rank we are currently processing
|
|
554
|
+
if has_bin_groups:
|
|
555
|
+
if not self._supports_manual_ranks_mapping:
|
|
556
|
+
raise RuntimeError(
|
|
557
|
+
"This MpiTool class does not supports ranks mapping."
|
|
558
|
+
)
|
|
559
|
+
self._complex_ranks_map = True
|
|
560
|
+
cursor0 = 0 # The first available "real" slot
|
|
561
|
+
group_cache = collections.defaultdict(list)
|
|
562
|
+
for a_bin in self.binaries:
|
|
563
|
+
if a_bin.group is None:
|
|
564
|
+
# Easy, the usual case
|
|
565
|
+
reserved = list(range(cursor0, cursor0 + a_bin.nprocs))
|
|
566
|
+
cursor0 += a_bin.nprocs
|
|
567
|
+
else:
|
|
568
|
+
reserved = group_cache.get(a_bin, [])
|
|
569
|
+
if not reserved:
|
|
570
|
+
# It is the first time this group of binaries is seen
|
|
571
|
+
# Find out what are the binaries in this group
|
|
572
|
+
bin_buddies = [
|
|
573
|
+
bin_b
|
|
574
|
+
for bin_b in self.binaries
|
|
575
|
+
if bin_b.group == a_bin.group
|
|
576
|
+
]
|
|
577
|
+
if all(
|
|
578
|
+
[
|
|
579
|
+
"nn" in bin_b.options
|
|
580
|
+
for bin_b in bin_buddies
|
|
581
|
+
]
|
|
582
|
+
):
|
|
583
|
+
# Each of the binary descriptions should define the number of nodes
|
|
584
|
+
max_nn = max(
|
|
585
|
+
[
|
|
586
|
+
bin_b.options["nn"]
|
|
587
|
+
for bin_b in bin_buddies
|
|
588
|
+
]
|
|
589
|
+
)
|
|
590
|
+
for i_node in range(max_nn):
|
|
591
|
+
for bin_b in bin_buddies:
|
|
592
|
+
if bin_b.options["nn"] > i_node:
|
|
593
|
+
group_cache[bin_b].extend(
|
|
594
|
+
range(
|
|
595
|
+
cursor0,
|
|
596
|
+
cursor0
|
|
597
|
+
+ bin_b.options["nnp"],
|
|
598
|
+
)
|
|
599
|
+
)
|
|
600
|
+
cursor0 += bin_b.options["nnp"]
|
|
601
|
+
else:
|
|
602
|
+
# If the number of nodes is not defined, revert to the number of tasks.
|
|
603
|
+
# This will probably result in strange results !
|
|
604
|
+
for bin_b in bin_buddies:
|
|
605
|
+
group_cache[bin_b].extend(
|
|
606
|
+
range(cursor0, cursor0 + bin_b.nprocs)
|
|
607
|
+
)
|
|
608
|
+
cursor0 += bin_b.nprocs
|
|
609
|
+
reserved = group_cache[a_bin]
|
|
610
|
+
for rank in range(a_bin.nprocs):
|
|
611
|
+
ranks_map[rank + cursor] = reserved[rank]
|
|
612
|
+
cursor += a_bin.nprocs
|
|
613
|
+
else:
|
|
614
|
+
# Just do nothing...
|
|
615
|
+
for a_bin in self.binaries:
|
|
616
|
+
for rank in range(a_bin.nprocs):
|
|
617
|
+
ranks_map[rank + cursor] = rank + cursor
|
|
618
|
+
cursor += a_bin.nprocs
|
|
619
|
+
# Then deal with distribution
|
|
620
|
+
do_bin_distribution = not all(
|
|
621
|
+
[b.distribution in (None, "continuous") for b in self.binaries]
|
|
622
|
+
)
|
|
623
|
+
if self._complex_ranks_map or do_bin_distribution:
|
|
624
|
+
if not self.envelope:
|
|
625
|
+
raise RuntimeError(
|
|
626
|
+
"Ranks mapping shoudl always be used within an envelope."
|
|
627
|
+
)
|
|
628
|
+
if do_bin_distribution:
|
|
629
|
+
if not self._supports_manual_ranks_mapping:
|
|
630
|
+
raise RuntimeError(
|
|
631
|
+
"This MpiTool class does not supports ranks mapping."
|
|
632
|
+
)
|
|
633
|
+
self._complex_ranks_map = True
|
|
634
|
+
if all(
|
|
635
|
+
[
|
|
636
|
+
"nn" in b.options and "nnp" in b.options
|
|
637
|
+
for b in self.envelope
|
|
638
|
+
]
|
|
639
|
+
):
|
|
640
|
+
# Extract node information
|
|
641
|
+
node_cursor = 0
|
|
642
|
+
nodes_id = list()
|
|
643
|
+
for e_bit in self.envelope:
|
|
644
|
+
for _ in range(e_bit.options["nn"]):
|
|
645
|
+
nodes_id.extend(
|
|
646
|
+
[
|
|
647
|
+
node_cursor,
|
|
648
|
+
]
|
|
649
|
+
* e_bit.options["nnp"]
|
|
650
|
+
)
|
|
651
|
+
node_cursor += 1
|
|
652
|
+
# Re-order ranks given the distribution
|
|
653
|
+
cursor = 0
|
|
654
|
+
for a_bin in self.binaries:
|
|
655
|
+
if a_bin.distribution == "roundrobin":
|
|
656
|
+
# The current list of ranks
|
|
657
|
+
actual_ranks = [
|
|
658
|
+
ranks_map[i]
|
|
659
|
+
for i in range(cursor, cursor + a_bin.nprocs)
|
|
660
|
+
]
|
|
661
|
+
# Find the node number associated with each rank
|
|
662
|
+
nodes_dict = collections.defaultdict(
|
|
663
|
+
collections.deque
|
|
664
|
+
)
|
|
665
|
+
for rank in actual_ranks:
|
|
666
|
+
nodes_dict[nodes_id[rank]].append(rank)
|
|
667
|
+
# Create a new list of ranks in a round-robin manner
|
|
668
|
+
actual_ranks = list()
|
|
669
|
+
iter_nodes = itertools.cycle(
|
|
670
|
+
sorted(nodes_dict.keys())
|
|
671
|
+
)
|
|
672
|
+
for _ in range(a_bin.nprocs):
|
|
673
|
+
av_ranks = None
|
|
674
|
+
while not av_ranks:
|
|
675
|
+
av_ranks = nodes_dict[next(iter_nodes)]
|
|
676
|
+
actual_ranks.append(av_ranks.popleft())
|
|
677
|
+
# Inject the result back
|
|
678
|
+
for i in range(a_bin.nprocs):
|
|
679
|
+
ranks_map[cursor + i] = actual_ranks[i]
|
|
680
|
+
cursor += a_bin.nprocs
|
|
681
|
+
else:
|
|
682
|
+
logger.warning(
|
|
683
|
+
"Cannot enforce binary distribution if the envelope"
|
|
684
|
+
+ "does not contain nn/nnp information"
|
|
685
|
+
)
|
|
686
|
+
# Cache the final result !
|
|
687
|
+
self._ranks_map_cache = ranks_map
|
|
688
|
+
return self._ranks_map_cache
|
|
689
|
+
|
|
690
|
+
@property
|
|
691
|
+
def _complex_ranks_mapping(self):
|
|
692
|
+
"""Is it a complex ranks mapping (e.g not the identity)."""
|
|
693
|
+
if self._complex_ranks_map is None:
|
|
694
|
+
# To initialise everything...
|
|
695
|
+
self._ranks_mapping
|
|
696
|
+
return self._complex_ranks_map
|
|
697
|
+
|
|
698
|
+
def _wrapstd_mkwrapper(self):
|
|
699
|
+
"""Generate the wrapper script used when wrapstd=True."""
|
|
700
|
+
if not self.mpiwrapstd:
|
|
701
|
+
return None
|
|
702
|
+
# Create the launchwrapper
|
|
703
|
+
with importlib.resources.path(
|
|
704
|
+
"vortex.algo.mpitools_templates",
|
|
705
|
+
self._wrapstd_wrapper_tpl,
|
|
706
|
+
) as tplpath:
|
|
707
|
+
wtpl = config.load_template(tplpath, encoding="utf-8")
|
|
708
|
+
with open(self._wrapstd_wrapper_name, "w", encoding="utf-8") as fhw:
|
|
709
|
+
fhw.write(
|
|
710
|
+
wtpl.substitute(
|
|
711
|
+
python=self.system.executable,
|
|
712
|
+
mpirankvariable=self._envelope_rank_var,
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
self.system.xperm(self._wrapstd_wrapper_name, force=True)
|
|
716
|
+
return self._wrapstd_wrapper_name
|
|
717
|
+
|
|
718
|
+
def _simple_mkcmdline(self, cmdl):
|
|
719
|
+
"""Builds the MPI command line when no envelope is used.
|
|
720
|
+
|
|
721
|
+
:param list[str] cmdl: the command line as a list
|
|
722
|
+
"""
|
|
723
|
+
effective = 0
|
|
724
|
+
wrapstd = self._wrapstd_mkwrapper()
|
|
725
|
+
for bin_obj in self.binaries:
|
|
726
|
+
if bin_obj.master is None:
|
|
727
|
+
raise MpiException("No master defined before launching MPI")
|
|
728
|
+
# If there are no options, do not bother...
|
|
729
|
+
if len(bin_obj.expanded_options()):
|
|
730
|
+
if effective > 0 and self.binsep:
|
|
731
|
+
cmdl.append(self.binsep)
|
|
732
|
+
e_options = self._hook_binary_mpiopts(
|
|
733
|
+
bin_obj, bin_obj.expanded_options()
|
|
734
|
+
)
|
|
735
|
+
for k in sorted(e_options.keys()):
|
|
736
|
+
if k in self.optmap:
|
|
737
|
+
cmdl.append(self.optprefix + str(self.optmap[k]))
|
|
738
|
+
if e_options[k] is not None:
|
|
739
|
+
cmdl.append(str(e_options[k]))
|
|
740
|
+
if self.optsep:
|
|
741
|
+
cmdl.append(self.optsep)
|
|
742
|
+
if wrapstd:
|
|
743
|
+
cmdl.append(wrapstd)
|
|
744
|
+
cmdl.append(bin_obj.master)
|
|
745
|
+
cmdl.extend(bin_obj.arguments)
|
|
746
|
+
effective += 1
|
|
747
|
+
|
|
748
|
+
def _envelope_fix_envelope_bit(self, e_bit, e_desc):
|
|
749
|
+
"""Set the envelope fake binary options."""
|
|
750
|
+
e_bit.options = {
|
|
751
|
+
k: v for k, v in e_desc.items() if k not in ("openmp", "np")
|
|
752
|
+
}
|
|
753
|
+
e_bit.master = self._envelope_wrapper_name
|
|
754
|
+
|
|
755
|
+
def _envelope_mkwrapper_todostack(self):
|
|
756
|
+
ranksidx = 0
|
|
757
|
+
ranks_bsize = dict()
|
|
758
|
+
todostack = dict()
|
|
759
|
+
for bin_obj in self.binaries:
|
|
760
|
+
if bin_obj.master is None:
|
|
761
|
+
raise MpiException("No master defined before launching MPI")
|
|
762
|
+
# If there are no options, do not bother...
|
|
763
|
+
if bin_obj.options and bin_obj.nprocs != 0:
|
|
764
|
+
if not bin_obj.nprocs:
|
|
765
|
+
raise ValueError(
|
|
766
|
+
"nranks must be provided when using envelopes"
|
|
767
|
+
)
|
|
768
|
+
for mpirank in range(ranksidx, ranksidx + bin_obj.nprocs):
|
|
769
|
+
if bin_obj.allowbind:
|
|
770
|
+
ranks_bsize[mpirank] = bin_obj.options.get("openmp", 1)
|
|
771
|
+
else:
|
|
772
|
+
ranks_bsize[mpirank] = -1
|
|
773
|
+
todostack[mpirank] = (
|
|
774
|
+
bin_obj.master,
|
|
775
|
+
bin_obj.arguments,
|
|
776
|
+
bin_obj.options.get("openmp", None),
|
|
777
|
+
)
|
|
778
|
+
ranksidx += bin_obj.nprocs
|
|
779
|
+
return todostack, ranks_bsize
|
|
780
|
+
|
|
781
|
+
def _envelope_mkwrapper_cpu_dispensers(self):
|
|
782
|
+
# Dispensers map
|
|
783
|
+
totalnodes = 0
|
|
784
|
+
ranks_idx = 0
|
|
785
|
+
dispensers_map = dict()
|
|
786
|
+
for e_bit in self.envelope:
|
|
787
|
+
if "nn" in e_bit.options and "nnp" in e_bit.options:
|
|
788
|
+
for _ in range(e_bit.options["nn"]):
|
|
789
|
+
cpu_disp = self.system.cpus_ids_dispenser(
|
|
790
|
+
topology=self.mpibind_topology
|
|
791
|
+
)
|
|
792
|
+
if not cpu_disp:
|
|
793
|
+
raise MpiException(
|
|
794
|
+
"Unable to detect the CPU layout with topology: {:s}".format(
|
|
795
|
+
self.mpibind_topology,
|
|
796
|
+
)
|
|
797
|
+
)
|
|
798
|
+
for _ in range(e_bit.options["nnp"]):
|
|
799
|
+
dispensers_map[ranks_idx] = (cpu_disp, totalnodes)
|
|
800
|
+
ranks_idx += 1
|
|
801
|
+
totalnodes += 1
|
|
802
|
+
else:
|
|
803
|
+
logger.error(
|
|
804
|
+
"Cannot compute a proper binding without nn/nnp information"
|
|
805
|
+
)
|
|
806
|
+
raise MpiException("Vortex binding error.")
|
|
807
|
+
return dispensers_map
|
|
808
|
+
|
|
809
|
+
def _envelope_mkwrapper_bindingstack(self, ranks_bsize):
|
|
810
|
+
binding_stack = dict()
|
|
811
|
+
binding_node = dict()
|
|
812
|
+
if self.bindingmethod:
|
|
813
|
+
dispensers_map = self._envelope_mkwrapper_cpu_dispensers()
|
|
814
|
+
# Actually generate the binding map
|
|
815
|
+
ranks_idx = 0
|
|
816
|
+
for e_bit in self.envelope:
|
|
817
|
+
for _ in range(e_bit.options["nn"]):
|
|
818
|
+
for _ in range(e_bit.options["nnp"]):
|
|
819
|
+
cpu_disp, i_node = dispensers_map[
|
|
820
|
+
self._ranks_mapping[ranks_idx]
|
|
821
|
+
]
|
|
822
|
+
if ranks_bsize.get(ranks_idx, 1) != -1:
|
|
823
|
+
try:
|
|
824
|
+
binding_stack[ranks_idx] = cpu_disp(
|
|
825
|
+
ranks_bsize.get(ranks_idx, 1)
|
|
826
|
+
)
|
|
827
|
+
except (StopIteration, IndexError):
|
|
828
|
+
# When CPU dispensers are exhausted (it might happened if more tasks
|
|
829
|
+
# than available CPUs are requested).
|
|
830
|
+
dispensers_map = (
|
|
831
|
+
self._envelope_mkwrapper_cpu_dispensers()
|
|
832
|
+
)
|
|
833
|
+
cpu_disp, i_node = dispensers_map[
|
|
834
|
+
self._ranks_mapping[ranks_idx]
|
|
835
|
+
]
|
|
836
|
+
binding_stack[ranks_idx] = cpu_disp(
|
|
837
|
+
ranks_bsize.get(ranks_idx, 1)
|
|
838
|
+
)
|
|
839
|
+
else:
|
|
840
|
+
binding_stack[ranks_idx] = set(
|
|
841
|
+
self.system.cpus_info.cpus.keys()
|
|
842
|
+
)
|
|
843
|
+
binding_node[ranks_idx] = i_node
|
|
844
|
+
ranks_idx += 1
|
|
845
|
+
return binding_stack, binding_node
|
|
846
|
+
|
|
847
|
+
def _envelope_mkwrapper_tplsubs(self, todostack, bindingstack):
|
|
848
|
+
return dict(
|
|
849
|
+
python=self.system.executable,
|
|
850
|
+
sitepath=self.system.path.join(self.ticket.glove.siteroot, "site"),
|
|
851
|
+
mpirankvariable=self._envelope_rank_var,
|
|
852
|
+
todolist=(
|
|
853
|
+
"\n".join(
|
|
854
|
+
[
|
|
855
|
+
" {:d}: ('{:s}', [{:s}], {:s}),".format(
|
|
856
|
+
mpi_r,
|
|
857
|
+
what[0],
|
|
858
|
+
", ".join(["'{:s}'".format(a) for a in what[1]]),
|
|
859
|
+
str(what[2]),
|
|
860
|
+
)
|
|
861
|
+
for mpi_r, what in sorted(todostack.items())
|
|
862
|
+
]
|
|
863
|
+
)
|
|
864
|
+
),
|
|
865
|
+
bindinglist=(
|
|
866
|
+
"\n".join(
|
|
867
|
+
[
|
|
868
|
+
" {:d}: [{:s}],".format(
|
|
869
|
+
mpi_r, ", ".join(["{:d}".format(a) for a in what])
|
|
870
|
+
)
|
|
871
|
+
for mpi_r, what in sorted(bindingstack.items())
|
|
872
|
+
]
|
|
873
|
+
)
|
|
874
|
+
),
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
def _envelope_mkwrapper(self, cmdl):
|
|
878
|
+
"""Generate the wrapper script used when an envelope is defined."""
|
|
879
|
+
# Generate the dictionary that associate rank numbers and programs
|
|
880
|
+
todostack, ranks_bsize = self._envelope_mkwrapper_todostack()
|
|
881
|
+
# Generate the binding stuff
|
|
882
|
+
bindingstack, bindingnode = self._envelope_mkwrapper_bindingstack(
|
|
883
|
+
ranks_bsize
|
|
884
|
+
)
|
|
885
|
+
# Print binding details
|
|
886
|
+
logger.debug(
|
|
887
|
+
"Vortex Envelope Mechanism is used"
|
|
888
|
+
+ (" & vortex binding is on." if bindingstack else ".")
|
|
889
|
+
)
|
|
890
|
+
env_info_head = "{:5s} {:24s} {:4s}".format(
|
|
891
|
+
"#rank", "binary_name", "#OMP"
|
|
892
|
+
)
|
|
893
|
+
env_info_fmt = "{:5d} {:24s} {:4s}"
|
|
894
|
+
if bindingstack:
|
|
895
|
+
env_info_head += " {:5s} {:s}".format("#node", "bindings_list")
|
|
896
|
+
env_info_fmt2 = " {:5d} {:s}"
|
|
897
|
+
binding_str = [env_info_head]
|
|
898
|
+
for i_rank in sorted(todostack):
|
|
899
|
+
entry_str = env_info_fmt.format(
|
|
900
|
+
i_rank,
|
|
901
|
+
self.system.path.basename(todostack[i_rank][0])[:24],
|
|
902
|
+
str(todostack[i_rank][2]),
|
|
903
|
+
)
|
|
904
|
+
if bindingstack:
|
|
905
|
+
entry_str += env_info_fmt2.format(
|
|
906
|
+
bindingnode[i_rank],
|
|
907
|
+
",".join([str(c) for c in sorted(bindingstack[i_rank])]),
|
|
908
|
+
)
|
|
909
|
+
binding_str.append(entry_str)
|
|
910
|
+
logger.debug(
|
|
911
|
+
"Here are the envelope details:\n%s", "\n".join(binding_str)
|
|
912
|
+
)
|
|
913
|
+
# Create the launchwrapper
|
|
914
|
+
with importlib.resources.path(
|
|
915
|
+
"vortex.algo.mpitools_templates",
|
|
916
|
+
self._envelope_wrapper_tpl,
|
|
917
|
+
) as tplpath:
|
|
918
|
+
wtpl = config.load_template(tplpath, encoding="utf-8")
|
|
919
|
+
with open(self._envelope_wrapper_name, "w", encoding="utf-8") as fhw:
|
|
920
|
+
fhw.write(
|
|
921
|
+
wtpl.substitute(
|
|
922
|
+
**self._envelope_mkwrapper_tplsubs(todostack, bindingstack)
|
|
923
|
+
)
|
|
924
|
+
)
|
|
925
|
+
self.system.xperm(self._envelope_wrapper_name, force=True)
|
|
926
|
+
return self._envelope_wrapper_name
|
|
927
|
+
|
|
928
|
+
def _envelope_mkcmdline(self, cmdl):
|
|
929
|
+
"""Builds the MPI command line when an envelope is used.
|
|
930
|
+
|
|
931
|
+
:param list[str] cmdl: the command line as a list
|
|
932
|
+
"""
|
|
933
|
+
self._envelope_mkwrapper(cmdl)
|
|
934
|
+
wrapstd = self._wrapstd_mkwrapper()
|
|
935
|
+
for effective, e_bit in enumerate(self.envelope):
|
|
936
|
+
if effective > 0 and self.binsep:
|
|
937
|
+
cmdl.append(self.binsep)
|
|
938
|
+
e_options = self._hook_binary_mpiopts(
|
|
939
|
+
e_bit, e_bit.expanded_options()
|
|
940
|
+
)
|
|
941
|
+
for k in sorted(e_options.keys()):
|
|
942
|
+
if k in self.optmap:
|
|
943
|
+
cmdl.append(self.optprefix + str(self.optmap[k]))
|
|
944
|
+
if e_options[k] is not None:
|
|
945
|
+
cmdl.append(str(e_options[k]))
|
|
946
|
+
self._envelope_mkcmdline_extra(cmdl)
|
|
947
|
+
if self.optsep:
|
|
948
|
+
cmdl.append(self.optsep)
|
|
949
|
+
if wrapstd:
|
|
950
|
+
cmdl.append(wrapstd)
|
|
951
|
+
cmdl.append(e_bit.master)
|
|
952
|
+
|
|
953
|
+
def _envelope_mkcmdline_extra(self, cmdl):
|
|
954
|
+
"""Possibly add extra options when building the envelope."""
|
|
955
|
+
pass
|
|
956
|
+
|
|
957
|
+
def mkcmdline(self):
|
|
958
|
+
"""Builds the MPI command line."""
|
|
959
|
+
cmdl = [
|
|
960
|
+
self.launcher,
|
|
961
|
+
]
|
|
962
|
+
for k, instances in sorted(self._reshaped_mpiopts().items()):
|
|
963
|
+
for instance in instances:
|
|
964
|
+
cmdl.append(self.optprefix + str(k))
|
|
965
|
+
for a_value in instance:
|
|
966
|
+
cmdl.append(str(a_value))
|
|
967
|
+
if self.envelope:
|
|
968
|
+
self._envelope_mkcmdline(cmdl)
|
|
969
|
+
else:
|
|
970
|
+
self._simple_mkcmdline(cmdl)
|
|
971
|
+
return cmdl
|
|
972
|
+
|
|
973
|
+
def clean(self, opts=None):
|
|
974
|
+
"""post-execution cleaning."""
|
|
975
|
+
if self.mpiwrapstd:
|
|
976
|
+
# Deal with standard output/error files
|
|
977
|
+
for outf in sorted(self.system.glob("vwrap_stdeo.*")):
|
|
978
|
+
rank = int(outf[12:])
|
|
979
|
+
with open(
|
|
980
|
+
outf,
|
|
981
|
+
encoding=locale.getlocale()[1] or "ascii",
|
|
982
|
+
errors="replace",
|
|
983
|
+
) as sfh:
|
|
984
|
+
for i, l in enumerate(sfh):
|
|
985
|
+
if i == 0:
|
|
986
|
+
self.system.highlight(
|
|
987
|
+
"rank {:d}: stdout/err".format(rank)
|
|
988
|
+
)
|
|
989
|
+
print(l.rstrip("\n"))
|
|
990
|
+
self.system.remove(outf)
|
|
991
|
+
if self.envelope and self.system.path.exists(
|
|
992
|
+
self._envelope_wrapper_name
|
|
993
|
+
):
|
|
994
|
+
self.system.remove(self._envelope_wrapper_name)
|
|
995
|
+
if self.mpiwrapstd:
|
|
996
|
+
self.system.remove(self._wrapstd_wrapper_name)
|
|
997
|
+
# Call the dedicated method en registered MPI binaries
|
|
998
|
+
for bin_obj in self.binaries:
|
|
999
|
+
bin_obj.clean(opts)
|
|
1000
|
+
|
|
1001
|
+
def find_namelists(self, opts=None):
|
|
1002
|
+
"""Find any namelists candidates in actual context inputs."""
|
|
1003
|
+
namcandidates = [
|
|
1004
|
+
x.rh
|
|
1005
|
+
for x in self.context.sequence.effective_inputs(
|
|
1006
|
+
kind=("namelist", "namelistfp")
|
|
1007
|
+
)
|
|
1008
|
+
]
|
|
1009
|
+
if opts is not None and "loop" in opts:
|
|
1010
|
+
namcandidates = [
|
|
1011
|
+
x
|
|
1012
|
+
for x in namcandidates
|
|
1013
|
+
if (
|
|
1014
|
+
hasattr(x.resource, "term")
|
|
1015
|
+
and x.resource.term == opts["loop"]
|
|
1016
|
+
)
|
|
1017
|
+
]
|
|
1018
|
+
else:
|
|
1019
|
+
logger.info("No loop option in current parallel execution.")
|
|
1020
|
+
self.system.highlight("Namelist candidates")
|
|
1021
|
+
for nam in namcandidates:
|
|
1022
|
+
nam.quickview()
|
|
1023
|
+
return namcandidates
|
|
1024
|
+
|
|
1025
|
+
def setup_namelist_delta(self, namcontents, namlocal):
|
|
1026
|
+
"""Abstract method for applying a delta: return False."""
|
|
1027
|
+
return False
|
|
1028
|
+
|
|
1029
|
+
def setup_namelists(self, opts=None):
|
|
1030
|
+
"""MPI information to be written in namelists."""
|
|
1031
|
+
for namrh in self.find_namelists(opts):
|
|
1032
|
+
namc = namrh.contents
|
|
1033
|
+
changed = self.setup_namelist_delta(
|
|
1034
|
+
namc, namrh.container.actualpath()
|
|
1035
|
+
)
|
|
1036
|
+
# Call the dedicated method en registered MPI binaries
|
|
1037
|
+
for bin_obj in self.binaries:
|
|
1038
|
+
changed = (
|
|
1039
|
+
bin_obj.setup_namelist_delta(
|
|
1040
|
+
namc, namrh.container.actualpath()
|
|
1041
|
+
)
|
|
1042
|
+
or changed
|
|
1043
|
+
)
|
|
1044
|
+
if changed:
|
|
1045
|
+
if namc.dumps_needs_update:
|
|
1046
|
+
logger.info(
|
|
1047
|
+
"Rewritting the %s namelists file.",
|
|
1048
|
+
namrh.container.actualpath(),
|
|
1049
|
+
)
|
|
1050
|
+
namc.rewrite(namrh.container)
|
|
1051
|
+
|
|
1052
|
+
def _logged_env_set(self, k, v):
|
|
1053
|
+
"""Set an environment variable *k* and emit a log message."""
|
|
1054
|
+
logger.info(
|
|
1055
|
+
'Setting the "%s" environment variable to "%s"', k.upper(), v
|
|
1056
|
+
)
|
|
1057
|
+
self.env[k] = v
|
|
1058
|
+
|
|
1059
|
+
def _logged_env_del(self, k):
|
|
1060
|
+
"""Delete the environment variable *k* and emit a log message."""
|
|
1061
|
+
logger.info('Deleting the "%s" environment variable', k.upper())
|
|
1062
|
+
del self.env[k]
|
|
1063
|
+
|
|
1064
|
+
def _environment_substitution_dict(self):
|
|
1065
|
+
"""Things that may be substituted in environment variables."""
|
|
1066
|
+
sdict = dict()
|
|
1067
|
+
mpilib_data = self._mpilib_data()
|
|
1068
|
+
if mpilib_data:
|
|
1069
|
+
sdict.update(mpilib=mpilib_data[0], mpibindir=mpilib_data[1])
|
|
1070
|
+
return sdict
|
|
1071
|
+
|
|
1072
|
+
def setup_environment(self, opts):
|
|
1073
|
+
"""MPI environment setup."""
|
|
1074
|
+
confdata = from_config(section="mpienv")
|
|
1075
|
+
envsub = self._environment_substitution_dict()
|
|
1076
|
+
for k, v in confdata.items():
|
|
1077
|
+
if k not in self.env:
|
|
1078
|
+
try:
|
|
1079
|
+
v = str(v).format(**envsub)
|
|
1080
|
+
except KeyError:
|
|
1081
|
+
logger.warning(
|
|
1082
|
+
"Substitution failed for the environment "
|
|
1083
|
+
+ "variable %s. Ignoring it.",
|
|
1084
|
+
k,
|
|
1085
|
+
)
|
|
1086
|
+
else:
|
|
1087
|
+
self._logged_env_set(k, v)
|
|
1088
|
+
# Call the dedicated method en registered MPI binaries
|
|
1089
|
+
for bin_obj in self.binaries:
|
|
1090
|
+
bin_obj.setup_environment(opts)
|
|
1091
|
+
|
|
1092
|
+
def setup(self, opts=None):
|
|
1093
|
+
"""Specific MPI settings to be applied before run."""
|
|
1094
|
+
self.setup_namelists(opts)
|
|
1095
|
+
if self.target is not None:
|
|
1096
|
+
self.setup_environment(opts)
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
class MpiBinaryDescription(footprints.FootprintBase):
|
|
1100
|
+
"""Root class for any :class:`MpiBinaryDescription` subclass."""
|
|
1101
|
+
|
|
1102
|
+
_collector = ("mpibinary",)
|
|
1103
|
+
_abstract = True
|
|
1104
|
+
_footprint = dict(
|
|
1105
|
+
info="Holds information about a given MPI binary",
|
|
1106
|
+
attr=dict(
|
|
1107
|
+
kind=dict(
|
|
1108
|
+
info="A free form description of the binary's type",
|
|
1109
|
+
values=[
|
|
1110
|
+
"basic",
|
|
1111
|
+
],
|
|
1112
|
+
),
|
|
1113
|
+
nodes=dict(
|
|
1114
|
+
info="The number of nodes for this MPI binary",
|
|
1115
|
+
type=int,
|
|
1116
|
+
optional=True,
|
|
1117
|
+
access="rwx",
|
|
1118
|
+
),
|
|
1119
|
+
tasks=dict(
|
|
1120
|
+
info="The number of tasks per node for this MPI binary",
|
|
1121
|
+
type=int,
|
|
1122
|
+
optional=True,
|
|
1123
|
+
access="rwx",
|
|
1124
|
+
),
|
|
1125
|
+
openmp=dict(
|
|
1126
|
+
info="The number of threads per task for this MPI binary",
|
|
1127
|
+
type=int,
|
|
1128
|
+
optional=True,
|
|
1129
|
+
access="rwx",
|
|
1130
|
+
),
|
|
1131
|
+
ranks=dict(
|
|
1132
|
+
info="The number of MPI ranks to use (only when working in an envelope)",
|
|
1133
|
+
type=int,
|
|
1134
|
+
optional=True,
|
|
1135
|
+
access="rwx",
|
|
1136
|
+
),
|
|
1137
|
+
allowbind=dict(
|
|
1138
|
+
info="Allow the MpiTool to bind this executable",
|
|
1139
|
+
type=bool,
|
|
1140
|
+
optional=True,
|
|
1141
|
+
default=True,
|
|
1142
|
+
),
|
|
1143
|
+
basics=dict(
|
|
1144
|
+
type=footprints.FPList,
|
|
1145
|
+
optional=True,
|
|
1146
|
+
default=footprints.FPList(
|
|
1147
|
+
["system", "env", "target", "context"]
|
|
1148
|
+
),
|
|
1149
|
+
),
|
|
1150
|
+
),
|
|
1151
|
+
)
|
|
1152
|
+
|
|
1153
|
+
def __init__(self, *args, **kw):
|
|
1154
|
+
"""After parent initialization, set master and options to undefined."""
|
|
1155
|
+
logger.debug("Abstract mpi tool init %s", self.__class__)
|
|
1156
|
+
super().__init__(*args, **kw)
|
|
1157
|
+
self._master = None
|
|
1158
|
+
self._arguments = ()
|
|
1159
|
+
self._options = None
|
|
1160
|
+
self._group = None
|
|
1161
|
+
|
|
1162
|
+
def __getattr__(self, key):
|
|
1163
|
+
"""Have a look to basics values provided by some proxy."""
|
|
1164
|
+
if key in self.basics:
|
|
1165
|
+
return getattr(self, "_" + key)
|
|
1166
|
+
else:
|
|
1167
|
+
raise AttributeError(
|
|
1168
|
+
"Attribute [%s] is not a basic mpitool attribute" % key
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
def import_basics(self, obj, attrs=None):
|
|
1172
|
+
"""Import some current values such as system, env, target and context from provided ``obj``."""
|
|
1173
|
+
if attrs is None:
|
|
1174
|
+
attrs = self.basics
|
|
1175
|
+
for k in [x for x in attrs if x in self.basics and hasattr(obj, x)]:
|
|
1176
|
+
setattr(self, "_" + k, getattr(obj, k))
|
|
1177
|
+
|
|
1178
|
+
def _get_options(self):
|
|
1179
|
+
"""Retrieve the current set of MPI options."""
|
|
1180
|
+
if self._options is None:
|
|
1181
|
+
self._set_options(None)
|
|
1182
|
+
return self._options
|
|
1183
|
+
|
|
1184
|
+
def _set_options(self, value=None):
|
|
1185
|
+
"""Input a raw list of MPI options."""
|
|
1186
|
+
self._options = dict()
|
|
1187
|
+
if value is None:
|
|
1188
|
+
value = dict()
|
|
1189
|
+
if self.ranks is not None:
|
|
1190
|
+
self._options["np"] = self.ranks
|
|
1191
|
+
if self.nodes is not None or self.tasks is not None:
|
|
1192
|
+
raise ValueError("Incompatible options provided.")
|
|
1193
|
+
else:
|
|
1194
|
+
if self.nodes is not None:
|
|
1195
|
+
self._options["nn"] = self.nodes
|
|
1196
|
+
if self.tasks is not None:
|
|
1197
|
+
self._options["nnp"] = self.tasks
|
|
1198
|
+
if self.openmp is not None:
|
|
1199
|
+
self._options["openmp"] = self.openmp
|
|
1200
|
+
for k, v in value.items():
|
|
1201
|
+
self._options[k.lstrip("-").lower()] = v
|
|
1202
|
+
|
|
1203
|
+
options = property(_get_options, _set_options)
|
|
1204
|
+
|
|
1205
|
+
def expanded_options(self):
|
|
1206
|
+
"""The MPI options actually used by the :class:`MpiTool` object to generate the command line."""
|
|
1207
|
+
options = self.options.copy()
|
|
1208
|
+
options.setdefault("np", self.nprocs)
|
|
1209
|
+
return options
|
|
1210
|
+
|
|
1211
|
+
def _get_group(self):
|
|
1212
|
+
"""The group the current binary belongs to (may be ``None``)."""
|
|
1213
|
+
return self._group
|
|
1214
|
+
|
|
1215
|
+
def _set_group(self, value):
|
|
1216
|
+
"""Set the binary's group."""
|
|
1217
|
+
self._group = value
|
|
1218
|
+
|
|
1219
|
+
group = property(_get_group, _set_group)
|
|
1220
|
+
|
|
1221
|
+
@property
|
|
1222
|
+
def nprocs(self):
|
|
1223
|
+
"""Figure out what is the effective total number of tasks."""
|
|
1224
|
+
if "np" in self.options:
|
|
1225
|
+
nbproc = int(self.options["np"])
|
|
1226
|
+
elif "nnp" in self.options and "nn" in self.options:
|
|
1227
|
+
nbproc = int(self.options.get("nnp")) * int(self.options.get("nn"))
|
|
1228
|
+
else:
|
|
1229
|
+
raise MpiException("Impossible to compute nprocs.")
|
|
1230
|
+
return nbproc
|
|
1231
|
+
|
|
1232
|
+
def _get_master(self):
|
|
1233
|
+
"""Retrieve the master binary name that should be used."""
|
|
1234
|
+
return self._master
|
|
1235
|
+
|
|
1236
|
+
def _set_master(self, master):
|
|
1237
|
+
"""Keep a copy of the master binary pathname."""
|
|
1238
|
+
self._master = master
|
|
1239
|
+
|
|
1240
|
+
master = property(_get_master, _set_master)
|
|
1241
|
+
|
|
1242
|
+
def _get_arguments(self):
|
|
1243
|
+
"""Retrieve the master's arguments list."""
|
|
1244
|
+
return self._arguments
|
|
1245
|
+
|
|
1246
|
+
def _set_arguments(self, args):
|
|
1247
|
+
"""Keep a copy of the master binary pathname."""
|
|
1248
|
+
if isinstance(args, str):
|
|
1249
|
+
self._arguments = args.split()
|
|
1250
|
+
elif isinstance(args, collections.abc.Iterable):
|
|
1251
|
+
self._arguments = [str(a) for a in args]
|
|
1252
|
+
else:
|
|
1253
|
+
raise ValueError("Improper *args* argument provided.")
|
|
1254
|
+
|
|
1255
|
+
arguments = property(_get_arguments, _set_arguments)
|
|
1256
|
+
|
|
1257
|
+
def clean(self, opts=None):
|
|
1258
|
+
"""Abstract method for post-execution cleaning."""
|
|
1259
|
+
pass
|
|
1260
|
+
|
|
1261
|
+
def setup_namelist_delta(self, namcontents, namlocal):
|
|
1262
|
+
"""Abstract method for applying a delta: return False."""
|
|
1263
|
+
return False
|
|
1264
|
+
|
|
1265
|
+
def setup_environment(self, opts):
|
|
1266
|
+
"""Abstract MPI environment setup."""
|
|
1267
|
+
pass
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
class MpiEnvelopeBit(MpiBinaryDescription):
|
|
1271
|
+
"""Set NPROC and NBPROC in namelists given the MPI distribution."""
|
|
1272
|
+
|
|
1273
|
+
_footprint = dict(
|
|
1274
|
+
attr=dict(
|
|
1275
|
+
kind=dict(
|
|
1276
|
+
values=[
|
|
1277
|
+
"basicenvelopebit",
|
|
1278
|
+
],
|
|
1279
|
+
),
|
|
1280
|
+
)
|
|
1281
|
+
)
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
class MpiBinary(MpiBinaryDescription):
|
|
1285
|
+
_footprint = dict(
|
|
1286
|
+
attr=dict(
|
|
1287
|
+
distribution=dict(
|
|
1288
|
+
info="Describes how the various nodes are distributed accross nodes",
|
|
1289
|
+
values=["continuous", "roundrobin"],
|
|
1290
|
+
optional=True,
|
|
1291
|
+
),
|
|
1292
|
+
)
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
class MpiBinaryBasic(MpiBinary):
|
|
1297
|
+
"""Set NPROC and NBPROC in namelists given the MPI distribution."""
|
|
1298
|
+
|
|
1299
|
+
_footprint = dict(
|
|
1300
|
+
attr=dict(
|
|
1301
|
+
kind=dict(
|
|
1302
|
+
values=[
|
|
1303
|
+
"basicsingle",
|
|
1304
|
+
],
|
|
1305
|
+
),
|
|
1306
|
+
)
|
|
1307
|
+
)
|
|
1308
|
+
|
|
1309
|
+
def setup_namelist_delta(self, namcontents, namlocal):
|
|
1310
|
+
"""Applying MPI profile on local namelist ``namlocal`` with contents namcontents."""
|
|
1311
|
+
namw = False
|
|
1312
|
+
# List of macros actualy used in the namelist
|
|
1313
|
+
nam_macros = set()
|
|
1314
|
+
for nam_block in namcontents.values():
|
|
1315
|
+
nam_macros.update(nam_block.macros())
|
|
1316
|
+
# Look for relevant once
|
|
1317
|
+
nprocs_macros = ("NPROC", "NBPROC", "NTASKS")
|
|
1318
|
+
if any([n in nam_macros for n in nprocs_macros]):
|
|
1319
|
+
for n in nprocs_macros:
|
|
1320
|
+
logger.info(
|
|
1321
|
+
"Setup macro %s=%s in %s", n, self.nprocs, namlocal
|
|
1322
|
+
)
|
|
1323
|
+
namcontents.setmacro(n, self.nprocs)
|
|
1324
|
+
namw = True
|
|
1325
|
+
return namw
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
class MpiBinaryIOServer(MpiBinary):
|
|
1329
|
+
"""Standard binary description for IO Server binaries."""
|
|
1330
|
+
|
|
1331
|
+
_footprint = dict(
|
|
1332
|
+
attr=dict(
|
|
1333
|
+
kind=dict(
|
|
1334
|
+
values=[
|
|
1335
|
+
"ioserv",
|
|
1336
|
+
],
|
|
1337
|
+
),
|
|
1338
|
+
)
|
|
1339
|
+
)
|
|
1340
|
+
|
|
1341
|
+
def __init__(self, *args, **kw):
|
|
1342
|
+
"""After parent initialization, set launcher value."""
|
|
1343
|
+
logger.debug("Abstract mpi tool init %s", self.__class__)
|
|
1344
|
+
super().__init__(*args, **kw)
|
|
1345
|
+
thisenv = env.current()
|
|
1346
|
+
if self.ranks is None:
|
|
1347
|
+
self.ranks = thisenv.VORTEX_IOSERVER_RANKS
|
|
1348
|
+
if self.nodes is None:
|
|
1349
|
+
self.nodes = thisenv.VORTEX_IOSERVER_NODES
|
|
1350
|
+
if self.tasks is None:
|
|
1351
|
+
self.tasks = thisenv.VORTEX_IOSERVER_TASKS
|
|
1352
|
+
if self.openmp is None:
|
|
1353
|
+
self.openmp = thisenv.VORTEX_IOSERVER_OPENMP
|
|
1354
|
+
|
|
1355
|
+
def expanded_options(self):
|
|
1356
|
+
"""The number of IO nodes may be 0: account for that."""
|
|
1357
|
+
if self.nprocs == 0:
|
|
1358
|
+
return dict()
|
|
1359
|
+
else:
|
|
1360
|
+
return super().expanded_options()
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
class MpiRun(MpiTool):
|
|
1364
|
+
"""Standard MPI launcher on most systems: `mpirun`."""
|
|
1365
|
+
|
|
1366
|
+
_footprint = dict(
|
|
1367
|
+
attr=dict(
|
|
1368
|
+
sysname=dict(values=["Linux", "Darwin", "UnitTestLinux"]),
|
|
1369
|
+
mpiname=dict(
|
|
1370
|
+
values=["mpirun", "mpiperso", "default"],
|
|
1371
|
+
remap=dict(default="mpirun"),
|
|
1372
|
+
),
|
|
1373
|
+
optsep=dict(
|
|
1374
|
+
default="",
|
|
1375
|
+
),
|
|
1376
|
+
optprefix=dict(
|
|
1377
|
+
default="-",
|
|
1378
|
+
),
|
|
1379
|
+
optmap=dict(default=footprints.FPDict(np="np", nnp="npernode")),
|
|
1380
|
+
binsep=dict(
|
|
1381
|
+
default=":",
|
|
1382
|
+
),
|
|
1383
|
+
)
|
|
1384
|
+
)
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
class SRun(MpiTool):
|
|
1388
|
+
"""SLURM's srun launcher."""
|
|
1389
|
+
|
|
1390
|
+
_footprint = dict(
|
|
1391
|
+
attr=dict(
|
|
1392
|
+
sysname=dict(values=["Linux", "UnitTestLinux"]),
|
|
1393
|
+
mpiname=dict(
|
|
1394
|
+
values=[
|
|
1395
|
+
"srun",
|
|
1396
|
+
],
|
|
1397
|
+
),
|
|
1398
|
+
optsep=dict(
|
|
1399
|
+
default="",
|
|
1400
|
+
),
|
|
1401
|
+
optprefix=dict(
|
|
1402
|
+
default="--",
|
|
1403
|
+
),
|
|
1404
|
+
optmap=dict(
|
|
1405
|
+
default=footprints.FPDict(
|
|
1406
|
+
nn="nodes", nnp="ntasks-per-node", np="ntasks"
|
|
1407
|
+
)
|
|
1408
|
+
),
|
|
1409
|
+
slurmversion=dict(type=int, optional=True),
|
|
1410
|
+
mpiwrapstd=dict(
|
|
1411
|
+
default=True,
|
|
1412
|
+
),
|
|
1413
|
+
bindingmethod=dict(
|
|
1414
|
+
info="How to bind the MPI processes",
|
|
1415
|
+
values=[
|
|
1416
|
+
"native",
|
|
1417
|
+
"vortex",
|
|
1418
|
+
],
|
|
1419
|
+
access="rwx",
|
|
1420
|
+
optional=True,
|
|
1421
|
+
doc_visibility=footprints.doc.visibility.ADVANCED,
|
|
1422
|
+
doc_zorder=-90,
|
|
1423
|
+
),
|
|
1424
|
+
)
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
_envelope_nodelist_name = "./global_envelope_nodelist"
|
|
1428
|
+
_envelope_rank_var = "SLURM_PROCID"
|
|
1429
|
+
_supports_manual_ranks_mapping = True
|
|
1430
|
+
|
|
1431
|
+
@property
|
|
1432
|
+
def _actual_slurmversion(self):
|
|
1433
|
+
"""Return the slurm major version number."""
|
|
1434
|
+
slurmversion = self.slurmversion or from_config(
|
|
1435
|
+
section="mpitool", key="slurmversion"
|
|
1436
|
+
)
|
|
1437
|
+
if not slurmversion:
|
|
1438
|
+
raise ValueError("No slurm version specified")
|
|
1439
|
+
return slurmversion
|
|
1440
|
+
|
|
1441
|
+
def _set_binaries_hack(self, binaries):
|
|
1442
|
+
"""Set the list of :class:`MpiBinaryDescription` objects associated with this instance."""
|
|
1443
|
+
if (
|
|
1444
|
+
not self.envelope
|
|
1445
|
+
and len(
|
|
1446
|
+
[binary for binary in binaries if binary.expanded_options()]
|
|
1447
|
+
)
|
|
1448
|
+
> 1
|
|
1449
|
+
):
|
|
1450
|
+
self._set_envelope_from_binaries()
|
|
1451
|
+
|
|
1452
|
+
def _valid_envelope(self, value):
|
|
1453
|
+
"""Tweak the envelope ddescription values."""
|
|
1454
|
+
for e in value:
|
|
1455
|
+
if not ("nn" in e and "nnp" in e):
|
|
1456
|
+
raise MpiException(
|
|
1457
|
+
"Srun needs a nn/nnp specification to build the envelope."
|
|
1458
|
+
)
|
|
1459
|
+
|
|
1460
|
+
def _set_envelope(self, value):
|
|
1461
|
+
"""Set the envelope description."""
|
|
1462
|
+
super()._set_envelope(value)
|
|
1463
|
+
if len(self._envelope) > 1 and self.bindingmethod not in (
|
|
1464
|
+
None,
|
|
1465
|
+
"vortex",
|
|
1466
|
+
):
|
|
1467
|
+
logger.warning("Resetting the binding method to 'Vortex'.")
|
|
1468
|
+
self.bindingmethod = "vortex"
|
|
1469
|
+
|
|
1470
|
+
envelope = property(MpiTool._get_envelope, _set_envelope)
|
|
1471
|
+
|
|
1472
|
+
def _set_binaries_envelope_hack(self, binaries):
|
|
1473
|
+
"""Tweak the envelope after binaries were setup."""
|
|
1474
|
+
if self.bindingmethod not in (None, "vortex"):
|
|
1475
|
+
openmps = {b.options.get("openmp", None) for b in binaries}
|
|
1476
|
+
if len(openmps) > 1:
|
|
1477
|
+
logger.warning(
|
|
1478
|
+
"Resetting the binding method to 'Vortex' because "
|
|
1479
|
+
+ "the number of threads is not uniform."
|
|
1480
|
+
)
|
|
1481
|
+
self.bindingmethod = "vortex"
|
|
1482
|
+
|
|
1483
|
+
@property
|
|
1484
|
+
def _cpubind_opt(self):
|
|
1485
|
+
return self.optprefix + (
|
|
1486
|
+
"cpu_bind" if self._actual_slurmversion < 18 else "cpu-bind"
|
|
1487
|
+
)
|
|
1488
|
+
|
|
1489
|
+
def _build_cpumask(self, cmdl, what, bsize):
|
|
1490
|
+
"""Add a --cpu-bind option if needed."""
|
|
1491
|
+
cmdl.append(self._cpubind_opt)
|
|
1492
|
+
if self.bindingmethod == "native":
|
|
1493
|
+
assert len(what) == 1, "Only one item is allowed."
|
|
1494
|
+
if what[0].allowbind:
|
|
1495
|
+
ids = self.system.cpus_ids_per_blocks(
|
|
1496
|
+
blocksize=bsize,
|
|
1497
|
+
topology=self.mpibind_topology,
|
|
1498
|
+
hexmask=True,
|
|
1499
|
+
)
|
|
1500
|
+
if not ids:
|
|
1501
|
+
raise MpiException(
|
|
1502
|
+
"Unable to detect the CPU layout with topology: {:s}".format(
|
|
1503
|
+
self.mpibind_topology,
|
|
1504
|
+
)
|
|
1505
|
+
)
|
|
1506
|
+
masklist = [
|
|
1507
|
+
m
|
|
1508
|
+
for _, m in zip(
|
|
1509
|
+
range(what[0].options["nnp"]), itertools.cycle(ids)
|
|
1510
|
+
)
|
|
1511
|
+
]
|
|
1512
|
+
cmdl.append("mask_cpu:" + ",".join(masklist))
|
|
1513
|
+
else:
|
|
1514
|
+
cmdl.append("none")
|
|
1515
|
+
else:
|
|
1516
|
+
cmdl.append("none")
|
|
1517
|
+
|
|
1518
|
+
def _simple_mkcmdline(self, cmdl):
|
|
1519
|
+
"""Builds the MPI command line when no envelope is used.
|
|
1520
|
+
|
|
1521
|
+
:param list[str] cmdl: the command line as a list
|
|
1522
|
+
"""
|
|
1523
|
+
target_bins = [
|
|
1524
|
+
binary
|
|
1525
|
+
for binary in self.binaries
|
|
1526
|
+
if len(binary.expanded_options())
|
|
1527
|
+
]
|
|
1528
|
+
self._build_cpumask(
|
|
1529
|
+
cmdl, target_bins, target_bins[0].options.get("openmp", 1)
|
|
1530
|
+
)
|
|
1531
|
+
super()._simple_mkcmdline(cmdl)
|
|
1532
|
+
|
|
1533
|
+
def _envelope_mkcmdline(self, cmdl):
|
|
1534
|
+
"""Builds the MPI command line when an envelope is used.
|
|
1535
|
+
|
|
1536
|
+
:param list[str] cmdl: the command line as a list
|
|
1537
|
+
"""
|
|
1538
|
+
# Simple case, only one envelope description
|
|
1539
|
+
openmps = {b.options.get("openmp", 1) for b in self.binaries}
|
|
1540
|
+
if (
|
|
1541
|
+
len(self.envelope) == 1
|
|
1542
|
+
and not self._complex_ranks_mapping
|
|
1543
|
+
and len(openmps) == 1
|
|
1544
|
+
):
|
|
1545
|
+
self._build_cpumask(cmdl, self.envelope, openmps.pop())
|
|
1546
|
+
super()._envelope_mkcmdline(cmdl)
|
|
1547
|
+
# Multiple entries... use the nodelist stuff :-(
|
|
1548
|
+
else:
|
|
1549
|
+
# Find all the available nodes and ranks
|
|
1550
|
+
base_nodelist = []
|
|
1551
|
+
totalnodes = 0
|
|
1552
|
+
totaltasks = 0
|
|
1553
|
+
availnodes = itertools.cycle(
|
|
1554
|
+
xlist_strings(
|
|
1555
|
+
self.env.SLURM_NODELIST
|
|
1556
|
+
if self._actual_slurmversion < 18
|
|
1557
|
+
else self.env.SLURM_JOB_NODELIST
|
|
1558
|
+
)
|
|
1559
|
+
)
|
|
1560
|
+
for e_bit in self.envelope:
|
|
1561
|
+
totaltasks += e_bit.nprocs
|
|
1562
|
+
for _ in range(e_bit.options["nn"]):
|
|
1563
|
+
availnode = next(availnodes)
|
|
1564
|
+
logger.debug("Node #%5d is: %s", totalnodes, availnode)
|
|
1565
|
+
base_nodelist.extend(
|
|
1566
|
+
[
|
|
1567
|
+
availnode,
|
|
1568
|
+
]
|
|
1569
|
+
* e_bit.options["nnp"]
|
|
1570
|
+
)
|
|
1571
|
+
totalnodes += 1
|
|
1572
|
+
# Re-order the nodelist based on the binary groups
|
|
1573
|
+
nodelist = list()
|
|
1574
|
+
for i_rank in range(len(base_nodelist)):
|
|
1575
|
+
if i_rank < len(self._ranks_mapping):
|
|
1576
|
+
nodelist.append(base_nodelist[self._ranks_mapping[i_rank]])
|
|
1577
|
+
else:
|
|
1578
|
+
nodelist.append(base_nodelist[i_rank])
|
|
1579
|
+
# Write it to the nodefile
|
|
1580
|
+
with open(self._envelope_nodelist_name, "w") as fhnl:
|
|
1581
|
+
fhnl.write("\n".join(nodelist))
|
|
1582
|
+
# Generate wrappers
|
|
1583
|
+
self._envelope_mkwrapper(cmdl)
|
|
1584
|
+
wrapstd = self._wrapstd_mkwrapper()
|
|
1585
|
+
# Update the command line
|
|
1586
|
+
cmdl.append(self.optprefix + "nodelist")
|
|
1587
|
+
cmdl.append(self._envelope_nodelist_name)
|
|
1588
|
+
cmdl.append(self.optprefix + "ntasks")
|
|
1589
|
+
cmdl.append(str(totaltasks))
|
|
1590
|
+
cmdl.append(self.optprefix + "distribution")
|
|
1591
|
+
cmdl.append("arbitrary")
|
|
1592
|
+
cmdl.append(self._cpubind_opt)
|
|
1593
|
+
cmdl.append("none")
|
|
1594
|
+
if wrapstd:
|
|
1595
|
+
cmdl.append(wrapstd)
|
|
1596
|
+
cmdl.append(e_bit.master)
|
|
1597
|
+
|
|
1598
|
+
def clean(self, opts=None):
|
|
1599
|
+
"""post-execution cleaning."""
|
|
1600
|
+
super().clean(opts)
|
|
1601
|
+
if self.envelope and len(self.envelope) > 1:
|
|
1602
|
+
self.system.remove(self._envelope_nodelist_name)
|
|
1603
|
+
|
|
1604
|
+
def _environment_substitution_dict(self): # @UnusedVariable
|
|
1605
|
+
"""Things that may be substituted in environment variables."""
|
|
1606
|
+
sdict = super()._environment_substitution_dict()
|
|
1607
|
+
shp = self.system.path
|
|
1608
|
+
# Detect the path to the srun command
|
|
1609
|
+
actlauncher = self.launcher
|
|
1610
|
+
if not shp.exists(self.launcher):
|
|
1611
|
+
actlauncher = self.system.which(actlauncher)
|
|
1612
|
+
if not actlauncher:
|
|
1613
|
+
logger.error("The SRun launcher could not be found.")
|
|
1614
|
+
return sdict
|
|
1615
|
+
sdict["srunpath"] = actlauncher
|
|
1616
|
+
# Detect the path to the PMI library
|
|
1617
|
+
pmilib = shp.normpath(
|
|
1618
|
+
shp.join(shp.dirname(actlauncher), "..", "lib64", "libpmi.so")
|
|
1619
|
+
)
|
|
1620
|
+
if not shp.exists(pmilib):
|
|
1621
|
+
pmilib = shp.normpath(
|
|
1622
|
+
shp.join(shp.dirname(actlauncher), "..", "lib", "libpmi.so")
|
|
1623
|
+
)
|
|
1624
|
+
if not shp.exists(pmilib):
|
|
1625
|
+
logger.error("Could not find a PMI library")
|
|
1626
|
+
return sdict
|
|
1627
|
+
sdict["pmilib"] = pmilib
|
|
1628
|
+
return sdict
|
|
1629
|
+
|
|
1630
|
+
def setup_environment(self, opts):
|
|
1631
|
+
"""Tweak the environment with some srun specific settings."""
|
|
1632
|
+
super().setup_environment(opts)
|
|
1633
|
+
if (
|
|
1634
|
+
self._complex_ranks_mapping
|
|
1635
|
+
and self._mpilib_identification()
|
|
1636
|
+
and self._mpilib_identification()[3] == "intelmpi"
|
|
1637
|
+
):
|
|
1638
|
+
logger.info(
|
|
1639
|
+
"(Sadly) with IntelMPI, I_MPI_SLURM_EXT=0 is needed when a complex arbitrary"
|
|
1640
|
+
+ "ranks distribution is used. Exporting it !"
|
|
1641
|
+
)
|
|
1642
|
+
self.env["I_MPI_SLURM_EXT"] = 0
|
|
1643
|
+
if len(self.binaries) == 1 and not self.envelope:
|
|
1644
|
+
omp = self.binaries[0].options.get("openmp", None)
|
|
1645
|
+
if omp is not None:
|
|
1646
|
+
self._logged_env_set("OMP_NUM_THREADS", omp)
|
|
1647
|
+
if self.bindingmethod == "native" and "OMP_PROC_BIND" not in self.env:
|
|
1648
|
+
self._logged_env_set("OMP_PROC_BIND", "true")
|
|
1649
|
+
# cleaning unwanted environment stuff
|
|
1650
|
+
unwanted = set()
|
|
1651
|
+
for k in self.env:
|
|
1652
|
+
if k.startswith("SLURM_"):
|
|
1653
|
+
k = k[6:]
|
|
1654
|
+
if (
|
|
1655
|
+
k in ("NTASKS", "NPROCS")
|
|
1656
|
+
or re.match("N?TASKS_PER_", k)
|
|
1657
|
+
or re.match("N?CPUS_PER_", k)
|
|
1658
|
+
):
|
|
1659
|
+
unwanted.add(k)
|
|
1660
|
+
for k in unwanted:
|
|
1661
|
+
self.env.delvar("SLURM_{:s}".format(k))
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
class SRunDDT(SRun):
|
|
1665
|
+
"""SLURM's srun launcher with ARM's DDT."""
|
|
1666
|
+
|
|
1667
|
+
_footprint = dict(
|
|
1668
|
+
attr=dict(
|
|
1669
|
+
mpiname=dict(
|
|
1670
|
+
values=[
|
|
1671
|
+
"srun-ddt",
|
|
1672
|
+
],
|
|
1673
|
+
),
|
|
1674
|
+
)
|
|
1675
|
+
)
|
|
1676
|
+
|
|
1677
|
+
_conf_suffix = "-ddt"
|
|
1678
|
+
|
|
1679
|
+
def mkcmdline(self):
|
|
1680
|
+
"""Add the DDT prefix command to the command line"""
|
|
1681
|
+
cmdl = super().mkcmdline()
|
|
1682
|
+
armtool = ArmForgeTool(self.ticket)
|
|
1683
|
+
for extra_c in reversed(
|
|
1684
|
+
armtool.ddt_prefix_cmd(
|
|
1685
|
+
sources=self.sources,
|
|
1686
|
+
workdir=self.system.path.dirname(self.binaries[0].master),
|
|
1687
|
+
)
|
|
1688
|
+
):
|
|
1689
|
+
cmdl.insert(0, extra_c)
|
|
1690
|
+
return cmdl
|
|
1691
|
+
|
|
1692
|
+
|
|
1693
|
+
class OmpiMpiRun(MpiTool):
|
|
1694
|
+
"""OpenMPI's mpirun launcher."""
|
|
1695
|
+
|
|
1696
|
+
_footprint = dict(
|
|
1697
|
+
attr=dict(
|
|
1698
|
+
sysname=dict(values=["Linux", "UnitTestLinux"]),
|
|
1699
|
+
mpiname=dict(
|
|
1700
|
+
values=[
|
|
1701
|
+
"openmpi",
|
|
1702
|
+
],
|
|
1703
|
+
),
|
|
1704
|
+
optsep=dict(
|
|
1705
|
+
default="",
|
|
1706
|
+
),
|
|
1707
|
+
optprefix=dict(
|
|
1708
|
+
default="-",
|
|
1709
|
+
),
|
|
1710
|
+
optmap=dict(
|
|
1711
|
+
default=footprints.FPDict(np="np", nnp="npernode", xopenmp="x")
|
|
1712
|
+
),
|
|
1713
|
+
binsep=dict(
|
|
1714
|
+
default=":",
|
|
1715
|
+
),
|
|
1716
|
+
mpiwrapstd=dict(
|
|
1717
|
+
default=True,
|
|
1718
|
+
),
|
|
1719
|
+
bindingmethod=dict(
|
|
1720
|
+
info="How to bind the MPI processes",
|
|
1721
|
+
values=[
|
|
1722
|
+
"native",
|
|
1723
|
+
"vortex",
|
|
1724
|
+
],
|
|
1725
|
+
optional=True,
|
|
1726
|
+
doc_visibility=footprints.doc.visibility.ADVANCED,
|
|
1727
|
+
doc_zorder=-90,
|
|
1728
|
+
),
|
|
1729
|
+
preexistingenv=dict(
|
|
1730
|
+
optional=True,
|
|
1731
|
+
type=bool,
|
|
1732
|
+
default=False,
|
|
1733
|
+
),
|
|
1734
|
+
)
|
|
1735
|
+
)
|
|
1736
|
+
|
|
1737
|
+
_envelope_rankfile_name = "./global_envelope_rankfile"
|
|
1738
|
+
_envelope_rank_var = "OMPI_COMM_WORLD_RANK"
|
|
1739
|
+
_supports_manual_ranks_mapping = True
|
|
1740
|
+
|
|
1741
|
+
def _get_launcher(self):
|
|
1742
|
+
"""Returns the name of the mpi tool to be used."""
|
|
1743
|
+
if self.mpilauncher:
|
|
1744
|
+
return self.mpilauncher
|
|
1745
|
+
else:
|
|
1746
|
+
mpi_data = self._mpilib_data()
|
|
1747
|
+
if mpi_data:
|
|
1748
|
+
return self.system.path.join(mpi_data[1], "mpirun")
|
|
1749
|
+
else:
|
|
1750
|
+
return self._launcher
|
|
1751
|
+
|
|
1752
|
+
launcher = property(_get_launcher, MpiTool._set_launcher)
|
|
1753
|
+
|
|
1754
|
+
def _set_binaries_hack(self, binaries):
|
|
1755
|
+
if not self.envelope and self.bindingmethod == "native":
|
|
1756
|
+
self._set_envelope_from_binaries()
|
|
1757
|
+
|
|
1758
|
+
def _valid_envelope(self, value):
|
|
1759
|
+
"""Tweak the envelope description values."""
|
|
1760
|
+
for e in value:
|
|
1761
|
+
if not ("nn" in e and "nnp" in e):
|
|
1762
|
+
raise MpiException(
|
|
1763
|
+
"OpenMPI/mpirun needs a nn/nnp specification "
|
|
1764
|
+
+ "to build the envelope."
|
|
1765
|
+
)
|
|
1766
|
+
|
|
1767
|
+
def _hook_binary_mpiopts(self, binary, options):
|
|
1768
|
+
openmp = options.pop("openmp", None)
|
|
1769
|
+
if openmp is not None:
|
|
1770
|
+
options["xopenmp"] = "OMP_NUM_THREADS={:d}".format(openmp)
|
|
1771
|
+
return options
|
|
1772
|
+
|
|
1773
|
+
def _simple_mkcmdline(self, cmdl):
|
|
1774
|
+
"""Builds the MPI command line when no envelope is used.
|
|
1775
|
+
|
|
1776
|
+
:param list[str] cmdl: the command line as a list
|
|
1777
|
+
"""
|
|
1778
|
+
if self.bindingmethod is not None:
|
|
1779
|
+
raise RuntimeError(
|
|
1780
|
+
"If bindingmethod is set, an enveloppe should allways be used."
|
|
1781
|
+
)
|
|
1782
|
+
super()._simple_mkcmdline(cmdl)
|
|
1783
|
+
|
|
1784
|
+
def _create_rankfile(self, rankslist, nodeslist, slotslist):
|
|
1785
|
+
rf_strings = []
|
|
1786
|
+
|
|
1787
|
+
def _dump_slot_string(slot_strings, s_start, s_end):
|
|
1788
|
+
if s_start == s_end:
|
|
1789
|
+
slot_strings.append("{:d}".format(s_start))
|
|
1790
|
+
else:
|
|
1791
|
+
slot_strings.append("{:d}-{:d}".format(s_start, s_end))
|
|
1792
|
+
|
|
1793
|
+
for rank, node, slot in zip(rankslist, nodeslist, slotslist):
|
|
1794
|
+
slot_strings = list()
|
|
1795
|
+
if slot:
|
|
1796
|
+
slot = sorted(slot)
|
|
1797
|
+
s_end = s_start = slot[0]
|
|
1798
|
+
for s in slot[1:]:
|
|
1799
|
+
if s_end + 1 == s:
|
|
1800
|
+
s_end = s
|
|
1801
|
+
else:
|
|
1802
|
+
_dump_slot_string(slot_strings, s_start, s_end)
|
|
1803
|
+
s_end = s_start = s
|
|
1804
|
+
_dump_slot_string(slot_strings, s_start, s_end)
|
|
1805
|
+
rf_strings.append(
|
|
1806
|
+
"rank {:d}={:s} slot={:s}".format(
|
|
1807
|
+
rank, node, ",".join(slot_strings)
|
|
1808
|
+
)
|
|
1809
|
+
)
|
|
1810
|
+
logger.info("self.preexistingenv = {}".format(self.preexistingenv))
|
|
1811
|
+
if self.preexistingenv and self.system.path.exists(
|
|
1812
|
+
self._envelope_rankfile_name
|
|
1813
|
+
):
|
|
1814
|
+
logger.info("envelope file found in the directory")
|
|
1815
|
+
else:
|
|
1816
|
+
if self.preexistingenv:
|
|
1817
|
+
logger.info(
|
|
1818
|
+
"preexistingenv set to true, but no envelope file found"
|
|
1819
|
+
)
|
|
1820
|
+
logger.info("Using vortex computed one")
|
|
1821
|
+
logger.debug(
|
|
1822
|
+
"Here is the rankfile content:\n%s", "\n".join(rf_strings)
|
|
1823
|
+
)
|
|
1824
|
+
with open(self._envelope_rankfile_name, mode="w") as tmp_rf:
|
|
1825
|
+
tmp_rf.write("\n".join(rf_strings))
|
|
1826
|
+
return self._envelope_rankfile_name
|
|
1827
|
+
|
|
1828
|
+
def _envelope_nodelist(self):
|
|
1829
|
+
"""Create the relative nodelist based on the envelope"""
|
|
1830
|
+
base_nodelist = []
|
|
1831
|
+
totalnodes = 0
|
|
1832
|
+
for e_bit in self.envelope:
|
|
1833
|
+
for i_node in range(e_bit.options["nn"]):
|
|
1834
|
+
logger.debug("Node #%5d is: +n%d", i_node, totalnodes)
|
|
1835
|
+
base_nodelist.extend(
|
|
1836
|
+
[
|
|
1837
|
+
"+n{:d}".format(totalnodes),
|
|
1838
|
+
]
|
|
1839
|
+
* e_bit.options["nnp"]
|
|
1840
|
+
)
|
|
1841
|
+
totalnodes += 1
|
|
1842
|
+
return base_nodelist
|
|
1843
|
+
|
|
1844
|
+
def _envelope_mkcmdline(self, cmdl):
|
|
1845
|
+
"""Builds the MPI command line when an envelope is used.
|
|
1846
|
+
|
|
1847
|
+
:param list[str] args: the command line as a list
|
|
1848
|
+
"""
|
|
1849
|
+
cmdl.append(self.optprefix + "oversubscribe")
|
|
1850
|
+
if self.bindingmethod in (None, "native"):
|
|
1851
|
+
# Generate the dictionary that associate rank numbers and programs
|
|
1852
|
+
todostack, ranks_bsize = self._envelope_mkwrapper_todostack()
|
|
1853
|
+
# Generate the binding stuff
|
|
1854
|
+
bindingstack, _ = self._envelope_mkwrapper_bindingstack(
|
|
1855
|
+
ranks_bsize
|
|
1856
|
+
)
|
|
1857
|
+
# Generate a relative nodelist
|
|
1858
|
+
base_nodelist = self._envelope_nodelist()
|
|
1859
|
+
# Generate the rankfile
|
|
1860
|
+
ranks = sorted(todostack)
|
|
1861
|
+
nodes = [base_nodelist[self._ranks_mapping[r]] for r in ranks]
|
|
1862
|
+
if bindingstack:
|
|
1863
|
+
slots = [bindingstack[r] for r in ranks]
|
|
1864
|
+
else:
|
|
1865
|
+
slots = [
|
|
1866
|
+
sorted(self.system.cpus_info.cpus.keys()),
|
|
1867
|
+
] * len(ranks)
|
|
1868
|
+
rfile = self._create_rankfile(ranks, nodes, slots)
|
|
1869
|
+
# Add the rankfile on the command line
|
|
1870
|
+
cmdl.append(self.optprefix + "rankfile")
|
|
1871
|
+
cmdl.append(rfile)
|
|
1872
|
+
# Add the "usual" call to binaries and setup OMP_NUM_THREADS values
|
|
1873
|
+
wrapstd = self._wrapstd_mkwrapper()
|
|
1874
|
+
for i_bin, a_bin in enumerate(self.binaries):
|
|
1875
|
+
if i_bin > 0:
|
|
1876
|
+
cmdl.append(self.binsep)
|
|
1877
|
+
openmp = a_bin.options.get("openmp", None)
|
|
1878
|
+
if openmp:
|
|
1879
|
+
cmdl.append(self.optprefix + "x")
|
|
1880
|
+
cmdl.append("OMP_NUM_THREADS={!s}".format(openmp))
|
|
1881
|
+
cmdl.append(self.optprefix + "np")
|
|
1882
|
+
cmdl.append(str(a_bin.nprocs))
|
|
1883
|
+
if wrapstd:
|
|
1884
|
+
cmdl.append(wrapstd)
|
|
1885
|
+
cmdl.append(a_bin.master)
|
|
1886
|
+
cmdl.extend(a_bin.arguments)
|
|
1887
|
+
else:
|
|
1888
|
+
# Generate a host file but let vortex deal with the rest...
|
|
1889
|
+
base_nodelist = self._envelope_nodelist()
|
|
1890
|
+
ranks = list(range(len(base_nodelist)))
|
|
1891
|
+
rfile = self._create_rankfile(
|
|
1892
|
+
ranks,
|
|
1893
|
+
[base_nodelist[self._ranks_mapping[r]] for r in ranks],
|
|
1894
|
+
[
|
|
1895
|
+
sorted(self.system.cpus_info.cpus.keys()),
|
|
1896
|
+
]
|
|
1897
|
+
* len(base_nodelist),
|
|
1898
|
+
)
|
|
1899
|
+
# Generate wrappers
|
|
1900
|
+
self._envelope_mkwrapper(cmdl)
|
|
1901
|
+
wrapstd = self._wrapstd_mkwrapper()
|
|
1902
|
+
# Update the command line
|
|
1903
|
+
cmdl.append(self.optprefix + "rankfile")
|
|
1904
|
+
cmdl.append(rfile)
|
|
1905
|
+
cmdl.append(self.optprefix + "np")
|
|
1906
|
+
cmdl.append(str(len(base_nodelist)))
|
|
1907
|
+
if wrapstd:
|
|
1908
|
+
cmdl.append(wrapstd)
|
|
1909
|
+
cmdl.append(self.envelope[0].master)
|
|
1910
|
+
|
|
1911
|
+
def clean(self, opts=None):
|
|
1912
|
+
"""post-execution cleaning."""
|
|
1913
|
+
super().clean(opts)
|
|
1914
|
+
if self.envelope:
|
|
1915
|
+
self.system.remove(self._envelope_rankfile_name)
|
|
1916
|
+
|
|
1917
|
+
def setup_environment(self, opts):
|
|
1918
|
+
"""Tweak the environment with some srun specific settings."""
|
|
1919
|
+
super().setup_environment(opts)
|
|
1920
|
+
if self.bindingmethod == "native" and "OMP_PROC_BIND" not in self.env:
|
|
1921
|
+
self._logged_env_set("OMP_PROC_BIND", "true")
|
|
1922
|
+
for libpath in self._mpilib_identification()[2]:
|
|
1923
|
+
logger.info('Adding "%s" to LD_LIBRARY_PATH', libpath)
|
|
1924
|
+
self.env.setgenericpath("LD_LIBRARY_PATH", libpath)
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
class OmpiMpiRunDDT(OmpiMpiRun):
|
|
1928
|
+
"""SLURM's srun launcher with ARM's DDT."""
|
|
1929
|
+
|
|
1930
|
+
_footprint = dict(
|
|
1931
|
+
attr=dict(
|
|
1932
|
+
mpiname=dict(
|
|
1933
|
+
values=[
|
|
1934
|
+
"openmpi-ddt",
|
|
1935
|
+
],
|
|
1936
|
+
),
|
|
1937
|
+
)
|
|
1938
|
+
)
|
|
1939
|
+
|
|
1940
|
+
_conf_suffix = "-ddt"
|
|
1941
|
+
|
|
1942
|
+
def mkcmdline(self):
|
|
1943
|
+
"""Add the DDT prefix command to the command line"""
|
|
1944
|
+
cmdl = super(OmpiMpiRun, self).mkcmdline()
|
|
1945
|
+
armtool = ArmForgeTool(self.ticket)
|
|
1946
|
+
for extra_c in reversed(
|
|
1947
|
+
armtool.ddt_prefix_cmd(
|
|
1948
|
+
sources=self.sources,
|
|
1949
|
+
workdir=self.system.path.dirname(self.binaries[0].master),
|
|
1950
|
+
)
|
|
1951
|
+
):
|
|
1952
|
+
cmdl.insert(0, extra_c)
|
|
1953
|
+
return cmdl
|