gstlal
1.13.0
Toggle main menu visibility
Loading...
Searching...
No Matches
python
dags
layers
__init__.py
1
# Copyright (C) 2020 Patrick Godwin (patrick.godwin@ligo.org)
2
#
3
# This program is free software; you can redistribute it and/or modify it
4
# under the terms of the GNU General Public License as published by the
5
# Free Software Foundation; either version 2 of the License, or (at your
6
# option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful, but
9
# WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11
# Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License along
14
# with this program; if not, write to the Free Software Foundation, Inc.,
15
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
17
18
from
collections.abc
import
Iterable
19
from
dataclasses
import
dataclass, field
20
import
itertools
21
import
os
22
from
typing
import
Optional, Union
23
24
import
htcondor
25
26
from
gstlal.dags
import
Argument, Option
27
from
gstlal.dags
import
util
as
dagutils
28
29
30
@dataclass
31
class
Layer
:
32
"""Defines a single layer (or set of related jobs) in an HTCondor DAG.
33
34
Stores submit configuration for a set of nodes as well as
35
providing functionality to determine the parent-child
36
relationships between nodes.
37
38
Parameters
39
----------
40
executable
41
The path of the executable to run.
42
name
43
The human-readable name of this node. Defaults to the basename
44
of the executable if not given.
45
universe
46
The execution environment for a job. Defaults to 'vanilla'.
47
retries
48
The number of retries given for a job. Defaults to 3.
49
transfer_files
50
Whether to leverage Condor file transfer for moving around
51
files. On by default.
52
dynamic_memory
53
Whether to dynamically increase memory request if jobs are
54
put on hold due to going over memory requested.
55
Off by default.
56
requirements
57
Additional key-value pairs in the submit description.
58
inputs
59
The arguments the nodes takes as inputs. If not specified,
60
they will be automatically generated when nodes are added
61
to this layer.
62
outputs
63
The arguments the nodes takes as outputs. If not specified,
64
they will be automatically generated when nodes are added
65
to this layer.
66
nodes
67
The nodes representing the layer. Nodes can be passed upon
68
instantiation or added to the layer after the fact via
69
Layer.append(node), Layer.extend(nodes), or Layer += node.
70
71
"""
72
executable: str
73
name: Optional[str] =
""
74
universe: Optional[str] =
"vanilla"
75
log_dir: Optional[str] =
"logs"
76
retries: Optional[int] = 3
77
transfer_files: Optional[bool] =
True
78
dynamic_memory: Optional[bool] =
False
79
requirements: Optional[dict] = field(default_factory=dict)
80
inputs: Optional[dict] = field(default_factory=dict)
81
outputs: Optional[dict] = field(default_factory=dict)
82
nodes: Optional[list] = field(default_factory=list)
83
84
def
__post_init__(self):
85
if
not
self.
name
:
86
self.
name
= os.path.basename(self.executable)
87
88
def
config(self):
89
# check that nodes are valid
90
self.
validate
()
91
92
# add base submit opts + requirements
93
submit_options = {
94
"universe"
: self.
universe
,
95
"executable"
: dagutils.which(self.executable),
96
"arguments"
: self.
_arguments
(),
97
}
98
99
# file submit opts
100
if
self.
universe
!=
"local"
:
101
submit_options.update({
102
"periodic_release"
:
"(HoldReasonCode == 5)"
,
103
**self.
requirements
,
104
})
105
if
self.
transfer_files
:
106
inputs = self.
_inputs
()
107
outputs = self.
_outputs
()
108
output_remaps = self.
_output_remaps
()
109
110
if
inputs
or
outputs:
111
submit_options[
"should_transfer_files"
] =
"YES"
112
submit_options[
"when_to_transfer_output"
] =
"ON_SUCCESS"
113
submit_options[
"success_exit_code"
] = 0
114
submit_options[
"preserve_relative_paths"
] =
True
115
if
inputs:
116
submit_options[
"transfer_input_files"
] = inputs
117
if
outputs:
118
submit_options[
"transfer_output_files"
] = outputs
119
submit_options[
"transfer_output_remaps"
] = f
'"{self._output_remaps()}"'
120
121
# log submit opts
122
submit_options[
"output"
] = f
"{self.log_dir}/$(nodename)-$(cluster)-$(process).out"
123
submit_options[
"error"
] = f
"{self.log_dir}/$(nodename)-$(cluster)-$(process).err"
124
125
# extra boilerplate submit opts
126
submit_options[
"notification"
] =
"never"
127
128
# set dynamic memory opts if requested
129
if
self.
dynamic_memory
:
130
base_memory = submit_options[
"request_memory"
]
131
submit_options[
"+MemoryUsage"
] = f
"( {base_memory} ) * 2 / 3"
132
submit_options[
"request_memory"
] =
"( MemoryUsage ) * 3 / 2"
133
submit_options[
"periodic_release"
] =
" || "
.join([
134
submit_options[
"periodic_release"
],
135
"((CurrentTime - EnteredCurrentStatus > 180) && (HoldReasonCode != 34))"
136
])
137
138
return
{
139
"name"
: self.
name
,
140
"submit_description"
: htcondor.Submit(submit_options),
141
"vars"
: self.
_vars
(),
142
"retries"
: self.
retries
,
143
}
144
145
def
append(self, node):
146
for
input_
in
node.inputs:
147
self.
inputs
.setdefault(input_.name, []).append(input_.argument)
148
for
output
in
node.outputs:
149
self.
outputs
.setdefault(output.name, []).append(output.argument)
150
self.
nodes
.append(node)
151
152
def
extend(self, nodes):
153
for
node
in
nodes:
154
self.
append
(node)
155
156
def
__iadd__(self, nodes):
157
if
isinstance(nodes, Iterable):
158
self.
extend
(nodes)
159
else
:
160
self.
append
(nodes)
161
return
self
162
163
def
validate(self):
164
assert
self.
nodes
,
"at least one node must be connected to this layer"
165
166
# check arg names across nodes are equal
167
args = [arg.name
for
arg
in
self.
nodes
[0].arguments]
168
for
node
in
self.
nodes
[:-1]:
169
assert
args == [arg.name
for
arg
in
node.arguments]
170
171
# check input/output names across nodes are equal
172
inputs = [arg.name
for
arg
in
self.
nodes
[0].inputs]
173
for
node
in
self.
nodes
[:-1]:
174
assert
inputs == [arg.name
for
arg
in
node.inputs]
175
outputs = [arg.name
for
arg
in
self.
nodes
[0].outputs]
176
for
node
in
self.
nodes
[:-1]:
177
assert
outputs == [arg.name
for
arg
in
node.outputs]
178
179
@property
180
def
has_dependencies(self):
181
return
any([node.requires
for
node
in
self.
nodes
])
182
183
def
_arguments(self):
184
args = [f
"$({arg.condor_name})"
for
arg
in
self.
nodes
[0].arguments]
185
io_args = []
186
io_opts = []
187
for
arg
in
itertools.chain(self.
nodes
[0].inputs, self.
nodes
[0].outputs):
188
if
not
arg.suppress:
189
if
isinstance(arg, Argument):
190
io_args.append(f
"$({arg.condor_name})"
)
191
else
:
192
io_opts.append(f
"$({arg.condor_name})"
)
193
return
" "
.join(itertools.chain(args, io_opts, io_args))
194
195
def
_inputs(self):
196
return
","
.join([f
"$(input_{arg.condor_name})"
for
arg
in
self.
nodes
[0].inputs])
197
198
def
_outputs(self):
199
return
","
.join([f
"$(output_{arg.condor_name})"
for
arg
in
self.
nodes
[0].outputs])
200
201
def
_output_remaps(self):
202
return
";"
.join([f
"$(output_{arg.condor_name}_remap)"
for
arg
in
self.
nodes
[0].outputs])
203
204
def
_vars(self):
205
allvars = []
206
for
i, node
in
enumerate(self.
nodes
):
207
nodevars = {
"nodename"
: f
"{self.name}_{i:05X}"
}
208
# add arguments which aren't suppressed
209
if
node.arguments:
210
nodevars.update({arg.condor_name: arg.vars()
for
arg
in
node.arguments
if
not
arg.suppress})
211
# then add arguments defined as 'inputs'. if file transfer is enabled,
212
# also define the $(input_{arg}) variable containing the files
213
if
node.inputs:
214
if
self.
transfer_files
:
215
# adjust file location for input files if they are absolute paths.
216
# condor will transfer the file /path/to/file.txt to the job's
217
# current working directory, so arguments should point to file.txt
218
args = {}
219
for
arg
in
node.inputs:
220
if
not
arg.suppress:
221
args[f
"{arg.condor_name}"
] = arg.vars(basename=os.path.isabs)
222
nodevars.update(args)
223
nodevars.update({f
"input_{arg.condor_name}"
: arg.files()
for
arg
in
node.inputs})
224
else
:
225
nodevars.update({f
"{arg.condor_name}"
: arg.vars()
for
arg
in
node.inputs
if
not
arg.suppress})
226
# finally, add arguments defined as 'outputs'. if file transfer is enabled,
227
# also define the $(output_{arg}) variable containing the files. if argument
228
# if not suppressed, some extra hoops are done with remaps to ensure that
229
# files are also saved to the right place. the main problem is that when jobs
230
# are submitted, the directory structure is present in the submit node but
231
# not the execute node, so when a job tries to create a file assuming the
232
# directories are there, the job fails. this gets around the issue by writing
233
# the files to the root directory then remaps them so they get stored in the
234
# right place after the job completes and files are transferred back
235
if
node.outputs:
236
for
arg
in
node.outputs:
237
if
not
arg.suppress:
238
basename = self.
transfer_files
and
arg.remap
239
nodevars.update({f
"{arg.condor_name}"
: arg.vars(basename=basename)})
240
if
self.
transfer_files
:
241
if
arg.remap:
242
nodevars.update({f
"output_{arg.condor_name}"
: arg.files(basename=
True
)})
243
nodevars.update({f
"output_{arg.condor_name}_remap"
: arg.remaps()})
244
else
:
245
nodevars.update({f
"output_{arg.condor_name}"
: arg.files()})
246
allvars.append(nodevars)
247
248
return
allvars
249
250
251
@dataclass
252
class
Node
:
253
"""Defines a single node (or job) in an HTCondor DAG.
254
255
Stores both the arguments used within a job as well
256
as capturing any inputs and outputs the job uses/creates.
257
258
Parameters
259
----------
260
arguments
261
The arguments the node uses which aren't I/O related.
262
inputs
263
The arguments the node takes as inputs.
264
outputs
265
The arguments the node takes as outputs.
266
267
"""
268
arguments: Optional[Union[Argument, Option, list]] = field(default_factory=list)
269
inputs: Optional[Union[Argument, Option, list]] = field(default_factory=list)
270
outputs: Optional[Union[Argument, Option, list]] = field(default_factory=list)
271
272
def
__post_init__(self):
273
if
isinstance(self.
arguments
, Argument)
or
isinstance(self.
arguments
, Option):
274
self.
arguments
= [self.
arguments
]
275
if
isinstance(self.
inputs
, Argument)
or
isinstance(self.
inputs
, Option):
276
self.
inputs
= [self.
inputs
]
277
if
isinstance(self.
outputs
, Argument)
or
isinstance(self.
outputs
, Option):
278
self.
outputs
= [self.
outputs
]
279
280
@property
281
def
requires
(self):
282
"""
283
Returns
284
-------
285
list
286
The inputs this node explicitly depends on to run.
287
288
"""
289
return
list(itertools.chain(*[input_.argument
for
input_
in
self.
inputs
if
input_.track]))
290
291
@property
292
def
provides
(self):
293
"""
294
Returns
295
-------
296
list
297
The outputs this node provides when it completes.
298
299
"""
300
return
list(itertools.chain(*[output.argument
for
output
in
self.
outputs
if
output.track]))
python.dags.layers.Layer
Definition
__init__.py:31
python.dags.layers.Layer.transfer_files
Optional transfer_files
Definition
__init__.py:77
python.dags.layers.Layer.requirements
Optional requirements
Definition
__init__.py:79
python.dags.layers.Layer.extend
extend(self, nodes)
Definition
__init__.py:152
python.dags.layers.Layer.outputs
Optional outputs
Definition
__init__.py:81
python.dags.layers.Layer._vars
_vars(self)
Definition
__init__.py:204
python.dags.layers.Layer._inputs
_inputs(self)
Definition
__init__.py:195
python.dags.layers.Layer.name
Optional name
Definition
__init__.py:73
python.dags.layers.Layer.nodes
Optional nodes
Definition
__init__.py:82
python.dags.layers.Layer.universe
Optional universe
Definition
__init__.py:74
python.dags.layers.Layer.append
append(self, node)
Definition
__init__.py:145
python.dags.layers.Layer._output_remaps
_output_remaps(self)
Definition
__init__.py:201
python.dags.layers.Layer.inputs
Optional inputs
Definition
__init__.py:80
python.dags.layers.Layer._outputs
_outputs(self)
Definition
__init__.py:198
python.dags.layers.Layer.retries
Optional retries
Definition
__init__.py:76
python.dags.layers.Layer.dynamic_memory
Optional dynamic_memory
Definition
__init__.py:78
python.dags.layers.Layer.validate
validate(self)
Definition
__init__.py:163
python.dags.layers.Layer._arguments
_arguments(self)
Definition
__init__.py:183
python.dags.layers.Node
Definition
__init__.py:252
python.dags.layers.Node.arguments
Optional arguments
Definition
__init__.py:268
python.dags.layers.Node.provides
provides(self)
Definition
__init__.py:292
python.dags.layers.Node.requires
requires(self)
Definition
__init__.py:281
python.dags.layers.Node.outputs
Optional outputs
Definition
__init__.py:270
python.dags.layers.Node.inputs
Optional inputs
Definition
__init__.py:269
Generated by
1.17.0