ATLAS Offline Software
Loading...
Searching...
No Matches
trfMPITools.py
Go to the documentation of this file.
1# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
2
8
9from enum import Enum
10from copy import deepcopy
11import os
12import re
13import logging
14import pprint
15import itertools as it
16from time import sleep
17
18from PyJobTransforms.trfExitCodes import trfExit
19import PyJobTransforms.trfExceptions as trfExceptions
20
21msg = logging.getLogger(__name__)
22
23mpiConfig = None
24
25
26class MPIType(Enum):
27 """MPI master, MPI worker, or not using MPI"""
28
29 NOMPI = 0
30 MPIMASTER = 1
31 MPIWORKER = 2
32
33
34def signalError(message):
35 msg.error(message)
37 trfExit.nameToCode("TRF_SETUP"), message
38 )
39
40
42 """Return MPI rank"""
43 if mpiConfig is not None:
44 return int(mpiConfig["rank"])
45 if "RANK" not in os.environ:
46 return -1
47 else:
48 try:
49 return int(os.environ["RANK"])
50 except ValueError:
51 signalError("$RANK environment variable is not an integer")
52 return -2 # Only here to placate PyRight
53
54
56 """Return MPI type"""
57 if mpiConfig is not None:
58 return mpiConfig["type"]
59 if "RANK" not in os.environ:
60 return MPIType.NOMPI
61 if getMPIRank() == 0:
62 return MPIType.MPIMASTER
63 else:
64 return MPIType.MPIWORKER
65
66
67def setupMPIConfig(output, dataDict):
68 """Check environment is correct if we are in MPI mode, and setup dictionaries"""
69 global mpiConfig
70 if "RANK" not in os.environ:
72 "Running in MPI mode but the $RANK environment variable is not set!"
73 )
74 rank = getMPIRank()
75 if not os.getcwd().endswith("rank-{}".format(getMPIRank())):
77 "Running in MPI mode with rank {0} but working directory is not called rank-{0}".format(
79 )
80 )
81 mpiType = getMPIType()
82 mpiConfig = {}
83 mpiConfig["rank"] = rank
84 mpiConfig["type"] = mpiType
85 mpiConfig["outputs"] = {
86 dataType: deepcopy(dataDict[dataType]) for dataType in output
87 }
88 # expand any [ ] lists in output filenames
89 output_proc_regex = re.compile(r"(.+)\‍[(.*)](.*)")
90 for v in mpiConfig["outputs"].values():
91 v.multipleOK = True
92 new_list = []
93 list_to_remove = []
94 for fn in v.value:
95 if ("[" in fn) and ("]" in fn):
96 match = output_proc_regex.match(fn)
97 new_list.extend(
98 [
99 f"{match.group(1)}{it}{match.group(3)}"
100 for it in match.group(2).split(",")
101 ]
102 )
103 list_to_remove.append(match.group(1))
104 else:
105 new_list.append(fn)
106 list_to_remove.append(fn)
107 v.value = new_list
108 v.list_to_remove = list(set(list_to_remove))
109
110
112 if getMPIType() == MPIType.NOMPI:
113 return True # validate if we're not in MPI mode
114 if getMPIRank() == 0:
115 return True # validate in rank 0
116 return False # don't validate in other ranks
117
118
120 return mpiConfig["outputs"].values()
121
122
124 """Merge outputs into rank 0"""
125 if mpiConfig is None:
126 msg.warning("trfMPITools.mergeOutputs called when we are not in MPI mode")
127 return
128 rank_dir_regex = re.compile("rank-([0-9]+)$")
129 rank_dirs = {
130 int(m.group(1)): m.string
131 for m in (rank_dir_regex.search(d.path) for d in os.scandir("..") if d.is_dir())
132 if m and int(m.group(1)) > 0
133 }
134 num_ranks = len(rank_dirs) + 1
135 # First wait for all ranks to reach this point so we don't start merging before some outputs are fully closed
136 open("athena_done", "a").close()
137 files_to_check = [
138 (rank, f"../rank-{rank}/athena_done") for rank in range(0, num_ranks)
139 ]
140 count = 0
141 files_to_check = list(
142 it.filterfalse(lambda f: os.path.exists(f[1]), files_to_check)
143 )
144 while files_to_check:
145 if count % 10 == 0 and getMPIRank() == 0:
146 msg.info(
147 f"{count // 10 + 1}: Waiting for all ranks to finish athena: {list(map(lambda x: x[0], files_to_check))}"
148 )
149 count += 1
150 sleep(6)
151 files_to_check = list(
152 it.filterfalse(lambda f: os.path.exists(f[1]), files_to_check)
153 )
154 # Now continue: First the logs
155 if getMPIRank() == 0:
156 import sqlite3 as sq3
157 from glob import glob
158
159 # Merge log databases
160 conn = sq3.connect("mpilog.db")
161 cur = conn.cursor()
162 tables = ["ranks", "files", "event_log"]
163 for db in glob("../rank-[1-9]*/mpilog.db"):
164 cur.execute("ATTACH DATABASE ? as db", (db,))
165 for table in tables:
166 upsert = "INSERT OR IGNORE" if table == "files" else "INSERT"
167 cur.execute(f"{upsert} INTO {table} SELECT * from db.{table}")
168 conn.commit()
169 cur.execute("DETACH DATABASE db")
170 conn.close()
171 # Then everything else
172 msg.info("Rank output directories are:\n{}".format(pprint.pformat(rank_dirs)))
173 all_merge_inputs = list(
174 map(
175 lambda f: f.path,
176 filter(
177 lambda f: f.is_file(),
178 it.chain.from_iterable(map(os.scandir, rank_dirs.values())),
179 ),
180 )
181 )
182 # Remove PoolFileCatalog
183 try:
184 os.remove("PoolFileCatalog.xml")
185 except FileNotFoundError:
186 pass
187 for dtype, defn in mpiConfig["outputs"].items():
188 if getMPIRank() == 0:
189 msg.info(f"Output type is {dtype}")
190 merge_helper = deepcopy(defn)
191 merge_helper.multipleOK = True
192 if getMPIRank() == 0:
193 for fn in defn.list_to_remove:
194 # remove empty files from rank 0
195 try:
196 os.remove(fn)
197 except FileNotFoundError:
198 pass
199 merge_lists = []
200 for fn in defn.value:
201 merge_inputs = sorted(filter(lambda s: s.endswith(fn), all_merge_inputs))
202 # Add to list
203 merge_helper.value.extend(merge_inputs)
204 merge_lists.append((fn, merge_inputs))
205 # Remove non-existent output files from mpiOutputs
206 defn.value = [x[0] for x in merge_lists if len(x[1]) >= 1]
207 # Merge each final output in a different rank
208 if getMPIRank() >= len(merge_lists):
209 msg.info(f"In rank {getMPIRank()}, not merging")
210 continue
211 for idx in range(getMPIRank(), len(merge_lists), num_ranks):
212 my_merge = merge_lists[idx]
213 if len(my_merge[1]) < 1:
214 msg.info(
215 f"In rank {getMPIRank()}, no inputs for ../rank-0/{my_merge[0]}"
216 )
217 continue
218 msg.info(
219 f"In rank {getMPIRank()}, merging into ../rank-0/{my_merge[0]}. Inputs are \n{pprint.pformat(my_merge[1])}"
220 )
221 try:
222 merge_helper.selfMerge(f"../rank-0/{my_merge[0]}", my_merge[1])
223 except Exception as e:
224 msg.error(
225 f"Merge failure in rank {getMPIRank()} merging into {my_merge[0]}: {e}"
226 )
227 with open("../rank-0/merge_failure", "a") as f:
228 f.write(
229 f"Merge failure in rank {getMPIRank()} merging into {my_merge[0]}: {e}\n"
230 )
231 # Create a file to indicate we are done
232 open("done_merging", "a").close()
233 if getMPIRank() == 0:
234 # In rank 0, wait until all other ranks have finished merging
235 files_to_check = [
236 (rank, f"../rank-{rank}/done_merging") for rank in range(0, num_ranks)
237 ]
238 count = 0
239 files_to_check = list(
240 it.filterfalse(lambda f: os.path.exists(f[1]), files_to_check)
241 )
242 while files_to_check:
243 if count % 10 == 0:
244 msg.info(
245 f"Waiting for all ranks to finish merging: {list(map(lambda x: x[0], files_to_check))}"
246 )
247 count += 1
248 sleep(6)
249 files_to_check = list(
250 it.filterfalse(lambda f: os.path.exists(f[1]), files_to_check)
251 )
252 if not os.path.exists("merge_failure"):
253 msg.info("All ranks done merging")
254 else:
255 msg.error("ERRORS WHILE MERGING")
256 raise RuntimeError("Output merging error")
STL class.
STL class.
std::vector< std::string > split(const std::string &s, const std::string &t=":")
Definition hcg.cxx:179
Module for transform exit codes.
setupMPIConfig(output, dataDict)