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.
Files changed (144) hide show
  1. vortex/__init__.py +159 -0
  2. vortex/algo/__init__.py +13 -0
  3. vortex/algo/components.py +2462 -0
  4. vortex/algo/mpitools.py +1953 -0
  5. vortex/algo/mpitools_templates/__init__.py +1 -0
  6. vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
  7. vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
  8. vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
  9. vortex/algo/serversynctools.py +171 -0
  10. vortex/config.py +112 -0
  11. vortex/data/__init__.py +19 -0
  12. vortex/data/abstractstores.py +1510 -0
  13. vortex/data/containers.py +835 -0
  14. vortex/data/contents.py +622 -0
  15. vortex/data/executables.py +275 -0
  16. vortex/data/flow.py +119 -0
  17. vortex/data/geometries.ini +2689 -0
  18. vortex/data/geometries.py +799 -0
  19. vortex/data/handlers.py +1230 -0
  20. vortex/data/outflow.py +67 -0
  21. vortex/data/providers.py +487 -0
  22. vortex/data/resources.py +207 -0
  23. vortex/data/stores.py +1390 -0
  24. vortex/data/sync_templates/__init__.py +0 -0
  25. vortex/gloves.py +309 -0
  26. vortex/layout/__init__.py +20 -0
  27. vortex/layout/contexts.py +577 -0
  28. vortex/layout/dataflow.py +1220 -0
  29. vortex/layout/monitor.py +969 -0
  30. vortex/nwp/__init__.py +14 -0
  31. vortex/nwp/algo/__init__.py +21 -0
  32. vortex/nwp/algo/assim.py +537 -0
  33. vortex/nwp/algo/clim.py +1086 -0
  34. vortex/nwp/algo/coupling.py +831 -0
  35. vortex/nwp/algo/eda.py +840 -0
  36. vortex/nwp/algo/eps.py +785 -0
  37. vortex/nwp/algo/forecasts.py +886 -0
  38. vortex/nwp/algo/fpserver.py +1303 -0
  39. vortex/nwp/algo/ifsnaming.py +463 -0
  40. vortex/nwp/algo/ifsroot.py +404 -0
  41. vortex/nwp/algo/monitoring.py +263 -0
  42. vortex/nwp/algo/mpitools.py +694 -0
  43. vortex/nwp/algo/odbtools.py +1258 -0
  44. vortex/nwp/algo/oopsroot.py +916 -0
  45. vortex/nwp/algo/oopstests.py +220 -0
  46. vortex/nwp/algo/request.py +660 -0
  47. vortex/nwp/algo/stdpost.py +1641 -0
  48. vortex/nwp/data/__init__.py +30 -0
  49. vortex/nwp/data/assim.py +380 -0
  50. vortex/nwp/data/boundaries.py +314 -0
  51. vortex/nwp/data/climfiles.py +521 -0
  52. vortex/nwp/data/configfiles.py +153 -0
  53. vortex/nwp/data/consts.py +954 -0
  54. vortex/nwp/data/ctpini.py +149 -0
  55. vortex/nwp/data/diagnostics.py +209 -0
  56. vortex/nwp/data/eda.py +147 -0
  57. vortex/nwp/data/eps.py +432 -0
  58. vortex/nwp/data/executables.py +1045 -0
  59. vortex/nwp/data/fields.py +111 -0
  60. vortex/nwp/data/gridfiles.py +380 -0
  61. vortex/nwp/data/logs.py +584 -0
  62. vortex/nwp/data/modelstates.py +363 -0
  63. vortex/nwp/data/monitoring.py +193 -0
  64. vortex/nwp/data/namelists.py +696 -0
  65. vortex/nwp/data/obs.py +840 -0
  66. vortex/nwp/data/oopsexec.py +74 -0
  67. vortex/nwp/data/providers.py +207 -0
  68. vortex/nwp/data/query.py +206 -0
  69. vortex/nwp/data/stores.py +160 -0
  70. vortex/nwp/data/surfex.py +337 -0
  71. vortex/nwp/syntax/__init__.py +9 -0
  72. vortex/nwp/syntax/stdattrs.py +437 -0
  73. vortex/nwp/tools/__init__.py +10 -0
  74. vortex/nwp/tools/addons.py +40 -0
  75. vortex/nwp/tools/agt.py +67 -0
  76. vortex/nwp/tools/bdap.py +59 -0
  77. vortex/nwp/tools/bdcp.py +41 -0
  78. vortex/nwp/tools/bdm.py +24 -0
  79. vortex/nwp/tools/bdmp.py +54 -0
  80. vortex/nwp/tools/conftools.py +1661 -0
  81. vortex/nwp/tools/drhook.py +66 -0
  82. vortex/nwp/tools/grib.py +294 -0
  83. vortex/nwp/tools/gribdiff.py +104 -0
  84. vortex/nwp/tools/ifstools.py +203 -0
  85. vortex/nwp/tools/igastuff.py +273 -0
  86. vortex/nwp/tools/mars.py +68 -0
  87. vortex/nwp/tools/odb.py +657 -0
  88. vortex/nwp/tools/partitioning.py +258 -0
  89. vortex/nwp/tools/satrad.py +71 -0
  90. vortex/nwp/util/__init__.py +6 -0
  91. vortex/nwp/util/async.py +212 -0
  92. vortex/nwp/util/beacon.py +40 -0
  93. vortex/nwp/util/diffpygram.py +447 -0
  94. vortex/nwp/util/ens.py +279 -0
  95. vortex/nwp/util/hooks.py +139 -0
  96. vortex/nwp/util/taskdeco.py +85 -0
  97. vortex/nwp/util/usepygram.py +697 -0
  98. vortex/nwp/util/usetnt.py +101 -0
  99. vortex/proxy.py +6 -0
  100. vortex/sessions.py +374 -0
  101. vortex/syntax/__init__.py +9 -0
  102. vortex/syntax/stdattrs.py +867 -0
  103. vortex/syntax/stddeco.py +185 -0
  104. vortex/toolbox.py +1117 -0
  105. vortex/tools/__init__.py +20 -0
  106. vortex/tools/actions.py +523 -0
  107. vortex/tools/addons.py +316 -0
  108. vortex/tools/arm.py +96 -0
  109. vortex/tools/compression.py +325 -0
  110. vortex/tools/date.py +27 -0
  111. vortex/tools/ddhpack.py +10 -0
  112. vortex/tools/delayedactions.py +782 -0
  113. vortex/tools/env.py +541 -0
  114. vortex/tools/folder.py +834 -0
  115. vortex/tools/grib.py +738 -0
  116. vortex/tools/lfi.py +953 -0
  117. vortex/tools/listings.py +423 -0
  118. vortex/tools/names.py +637 -0
  119. vortex/tools/net.py +2124 -0
  120. vortex/tools/odb.py +10 -0
  121. vortex/tools/parallelism.py +368 -0
  122. vortex/tools/prestaging.py +210 -0
  123. vortex/tools/rawfiles.py +10 -0
  124. vortex/tools/schedulers.py +480 -0
  125. vortex/tools/services.py +940 -0
  126. vortex/tools/storage.py +996 -0
  127. vortex/tools/surfex.py +61 -0
  128. vortex/tools/systems.py +3976 -0
  129. vortex/tools/targets.py +440 -0
  130. vortex/util/__init__.py +9 -0
  131. vortex/util/config.py +1122 -0
  132. vortex/util/empty.py +24 -0
  133. vortex/util/helpers.py +216 -0
  134. vortex/util/introspection.py +69 -0
  135. vortex/util/iosponge.py +80 -0
  136. vortex/util/roles.py +49 -0
  137. vortex/util/storefunctions.py +129 -0
  138. vortex/util/structs.py +26 -0
  139. vortex/util/worker.py +162 -0
  140. vortex_nwp-2.0.0.dist-info/METADATA +67 -0
  141. vortex_nwp-2.0.0.dist-info/RECORD +144 -0
  142. vortex_nwp-2.0.0.dist-info/WHEEL +5 -0
  143. vortex_nwp-2.0.0.dist-info/licenses/LICENSE +517 -0
  144. vortex_nwp-2.0.0.dist-info/top_level.txt +1 -0
@@ -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