lsst-ctrl-mpexec 29.2025.3400__py3-none-any.whl → 29.2025.3600__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.
- lsst/ctrl/mpexec/__init__.py +0 -1
- lsst/ctrl/mpexec/cli/butler_factory.py +253 -95
- lsst/ctrl/mpexec/cli/cmd/commands.py +2 -2
- lsst/ctrl/mpexec/cli/opt/optionGroups.py +0 -1
- lsst/ctrl/mpexec/cli/opt/options.py +0 -7
- lsst/ctrl/mpexec/cli/script/pre_exec_init_qbb.py +25 -12
- lsst/ctrl/mpexec/cli/script/qgraph.py +177 -89
- lsst/ctrl/mpexec/cli/script/run.py +211 -99
- lsst/ctrl/mpexec/cli/script/run_qbb.py +166 -31
- lsst/ctrl/mpexec/cli/utils.py +49 -0
- lsst/ctrl/mpexec/showInfo.py +17 -15
- lsst/ctrl/mpexec/version.py +1 -1
- {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/METADATA +1 -1
- {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/RECORD +22 -23
- lsst/ctrl/mpexec/cmdLineFwk.py +0 -534
- {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/WHEEL +0 -0
- {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/entry_points.txt +0 -0
- {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/licenses/COPYRIGHT +0 -0
- {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/licenses/LICENSE +0 -0
- {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/licenses/bsd_license.txt +0 -0
- {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/licenses/gpl-v3.0.txt +0 -0
- {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/top_level.txt +0 -0
- {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/zip-safe +0 -0
|
@@ -25,53 +25,67 @@
|
|
|
25
25
|
# You should have received a copy of the GNU General Public License
|
|
26
26
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
27
27
|
|
|
28
|
-
import
|
|
29
|
-
from types import SimpleNamespace
|
|
28
|
+
from __future__ import annotations
|
|
30
29
|
|
|
31
|
-
from
|
|
30
|
+
from collections.abc import Iterable
|
|
31
|
+
from typing import TYPE_CHECKING, Literal
|
|
32
|
+
|
|
33
|
+
import astropy.units as u
|
|
34
|
+
|
|
35
|
+
import lsst.utils.timer
|
|
36
|
+
from lsst.pipe.base import ExecutionResources, QuantumGraph, TaskFactory
|
|
37
|
+
from lsst.pipe.base.mp_graph_executor import MPGraphExecutor
|
|
38
|
+
from lsst.pipe.base.single_quantum_executor import SingleQuantumExecutor
|
|
39
|
+
from lsst.resources import ResourcePath, ResourcePathExpression
|
|
40
|
+
from lsst.utils.doImport import doImportType
|
|
41
|
+
from lsst.utils.iteration import ensure_iterable
|
|
42
|
+
from lsst.utils.logging import getLogger
|
|
32
43
|
from lsst.utils.threads import disable_implicit_threading
|
|
33
44
|
|
|
34
|
-
from
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
45
|
+
from ..butler_factory import ButlerFactory
|
|
46
|
+
from ..utils import MP_TIMEOUT
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from lsst.pipe.base.execution_graph_fixup import ExecutionGraphFixup
|
|
50
|
+
|
|
51
|
+
_LOG = getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run(
|
|
55
|
+
qg: QuantumGraph,
|
|
56
|
+
*,
|
|
57
|
+
task_factory: TaskFactory | None = None,
|
|
58
|
+
pdb: str | None,
|
|
59
|
+
graph_fixup: str,
|
|
60
|
+
init_only: bool,
|
|
61
|
+
no_versions: bool,
|
|
62
|
+
processes: int,
|
|
63
|
+
start_method: Literal["spawn", "forkserver"] | None,
|
|
64
|
+
profile: str,
|
|
65
|
+
register_dataset_types: bool,
|
|
66
|
+
skip_init_writes: bool,
|
|
67
|
+
timeout: int | None,
|
|
68
|
+
butler_config: ResourcePathExpression,
|
|
69
|
+
input: Iterable[str] | str,
|
|
70
|
+
output: str | None,
|
|
71
|
+
output_run: str | None,
|
|
72
|
+
extend_run: bool,
|
|
73
|
+
replace_run: bool,
|
|
74
|
+
prune_replaced: str | None,
|
|
75
|
+
data_query: str | None,
|
|
76
|
+
skip_existing_in: Iterable[str] | None,
|
|
77
|
+
skip_existing: bool,
|
|
78
|
+
debug: bool,
|
|
79
|
+
fail_fast: bool,
|
|
80
|
+
clobber_outputs: bool,
|
|
81
|
+
summary: ResourcePathExpression | None,
|
|
82
|
+
enable_implicit_threading: bool,
|
|
69
83
|
cores_per_quantum: int,
|
|
70
|
-
memory_per_quantum: str,
|
|
71
|
-
rebase,
|
|
84
|
+
memory_per_quantum: str | None,
|
|
85
|
+
rebase: bool,
|
|
72
86
|
raise_on_partial_outputs: bool,
|
|
73
|
-
**kwargs,
|
|
74
|
-
):
|
|
87
|
+
**kwargs: object,
|
|
88
|
+
) -> None:
|
|
75
89
|
"""Implement the command line interface `pipetask run` subcommand.
|
|
76
90
|
|
|
77
91
|
Should only be called by command line tools and unit test code that test
|
|
@@ -79,8 +93,13 @@ def run( # type: ignore
|
|
|
79
93
|
|
|
80
94
|
Parameters
|
|
81
95
|
----------
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
qg : `lsst.pipe.base.QuantumGraph`
|
|
97
|
+
A QuantumGraph generated by a previous subcommand.
|
|
98
|
+
task_factory : `lsst.pipe.base.TaskFactory`, optional
|
|
99
|
+
A custom task factory to use.
|
|
100
|
+
pdb : `str`, optional
|
|
101
|
+
Debugger to import and use (via the ``post_mortem`` function) in the
|
|
102
|
+
event of an exception.
|
|
84
103
|
graph_fixup : `str`
|
|
85
104
|
The name of the class or factory method which makes an instance used
|
|
86
105
|
for execution graph fixup.
|
|
@@ -96,8 +115,6 @@ def run( # type: ignore
|
|
|
96
115
|
one for current platform.
|
|
97
116
|
profile : `str`
|
|
98
117
|
File name to dump cProfile information to.
|
|
99
|
-
qgraphObj : `lsst.pipe.base.QuantumGraph`
|
|
100
|
-
A QuantumGraph generated by a previous subcommand.
|
|
101
118
|
register_dataset_types : `bool`
|
|
102
119
|
If true, register DatasetTypes that do not already exist in the
|
|
103
120
|
Registry.
|
|
@@ -106,21 +123,18 @@ def run( # type: ignore
|
|
|
106
123
|
schemas).
|
|
107
124
|
timeout : `int`
|
|
108
125
|
Timeout for multiprocessing; maximum wall time (sec).
|
|
109
|
-
butler_config :
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
pairs used to init or update the `lsst.daf.butler.Config` instance. If
|
|
113
|
-
`Config`, it is the object used to configure a Butler.
|
|
114
|
-
input : `list` [ `str` ]
|
|
126
|
+
butler_config : convertible to `lsst.resources.ResourcePath`
|
|
127
|
+
Path to butler repository configuration.
|
|
128
|
+
input : `~collections.abc.Iterable` [ `str` ] or `None`
|
|
115
129
|
List of names of the input collection(s).
|
|
116
|
-
output : `str`
|
|
130
|
+
output : `str` or `None`
|
|
117
131
|
Name of the output CHAINED collection. This may either be an existing
|
|
118
132
|
CHAINED collection to use as both input and output (if `input` is
|
|
119
133
|
`None`), or a new CHAINED collection created to include all inputs
|
|
120
134
|
(if `input` is not `None`). In both cases, the collection's children
|
|
121
135
|
will start with an output RUN collection that directly holds all new
|
|
122
136
|
datasets (see `output_run`).
|
|
123
|
-
output_run : `str`
|
|
137
|
+
output_run : `str` or `None`
|
|
124
138
|
Name of the new output RUN collection. If not provided then `output`
|
|
125
139
|
must be provided and a new RUN collection will be created by appending
|
|
126
140
|
a timestamp to the value passed with `output`. If this collection
|
|
@@ -136,13 +150,14 @@ def run( # type: ignore
|
|
|
136
150
|
development, but it does not delete the datasets associated with the
|
|
137
151
|
replaced run unless `prune-replaced` is also True. Requires `output`,
|
|
138
152
|
and `extend_run` must be `None`.
|
|
139
|
-
prune_replaced :
|
|
153
|
+
prune_replaced : `str` or `None`
|
|
140
154
|
If not `None`, delete the datasets in the collection replaced by
|
|
141
155
|
`replace_run`, either just from the datastore ("unstore") or by
|
|
142
|
-
removing them and the RUN completely ("purge"). Requires
|
|
156
|
+
removing them and the RUN completely ("purge"). Requires
|
|
157
|
+
``replace_run`` to be `True`.
|
|
143
158
|
data_query : `str`
|
|
144
159
|
User query selection expression.
|
|
145
|
-
skip_existing_in : `
|
|
160
|
+
skip_existing_in : `~collections.abc.Iterable` [ `str` ] or `None`
|
|
146
161
|
Accepts list of collections, if all Quantum outputs already exist in
|
|
147
162
|
the specified list of collections then that Quantum will be excluded
|
|
148
163
|
from the QuantumGraph.
|
|
@@ -160,14 +175,6 @@ def run( # type: ignore
|
|
|
160
175
|
given.
|
|
161
176
|
summary : `str`
|
|
162
177
|
File path to store job report in JSON format.
|
|
163
|
-
mock : `bool`, optional
|
|
164
|
-
If `True` then run mock pipeline instead of real one. Ignored if an
|
|
165
|
-
existing QuantumGraph is provided.
|
|
166
|
-
unmocked_dataset_types : `collections.abc.Sequence` [ `str` ]
|
|
167
|
-
List of overall-input dataset types that should not be mocked.
|
|
168
|
-
Ignored if an existing QuantumGraph is provided.
|
|
169
|
-
mock_failure : `~collections.abc.Sequence`, optional
|
|
170
|
-
List of quanta that should raise exceptions.
|
|
171
178
|
enable_implicit_threading : `bool`, optional
|
|
172
179
|
If `True`, do not disable implicit threading by third-party libraries.
|
|
173
180
|
Implicit threading is always disabled during actual quantum execution
|
|
@@ -183,58 +190,163 @@ def run( # type: ignore
|
|
|
183
190
|
the ``inputs``.
|
|
184
191
|
raise_on_partial_outputs : `bool`
|
|
185
192
|
Consider partial outputs an error instead of a success.
|
|
186
|
-
**kwargs : `
|
|
193
|
+
**kwargs : `object`
|
|
187
194
|
Ignored; click commands may accept options for more than one script
|
|
188
195
|
function and pass all the option kwargs to each of the script functions
|
|
189
196
|
which ignore these unused kwargs.
|
|
190
197
|
"""
|
|
191
198
|
# Fork option still exists for compatibility but we use spawn instead.
|
|
192
|
-
if start_method == "fork":
|
|
193
|
-
start_method = "spawn"
|
|
194
|
-
|
|
199
|
+
if start_method == "fork": # type: ignore[comparison-overlap]
|
|
200
|
+
start_method = "spawn" # type: ignore[unreachable]
|
|
201
|
+
_LOG.warning("Option --start-method=fork is unsafe and no longer supported, using spawn instead.")
|
|
195
202
|
|
|
196
203
|
if not enable_implicit_threading:
|
|
197
204
|
disable_implicit_threading()
|
|
198
205
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
206
|
+
skip_existing_in = tuple(skip_existing_in) if skip_existing_in is not None else ()
|
|
207
|
+
if data_query is None:
|
|
208
|
+
data_query = ""
|
|
209
|
+
inputs = list(ensure_iterable(input)) if input else []
|
|
210
|
+
del input
|
|
211
|
+
enable_lsst_debug = debug
|
|
212
|
+
del debug
|
|
213
|
+
|
|
214
|
+
# If we have no output run specified, use the one from the graph rather
|
|
215
|
+
# than letting a new timestamped run be created.
|
|
216
|
+
if not output_run and qg.metadata and (output_run := qg.metadata.get("output_run")):
|
|
217
|
+
output_run = output_run
|
|
218
|
+
|
|
219
|
+
# Check that output run defined on command line is consistent with
|
|
220
|
+
# quantum graph.
|
|
221
|
+
if output_run and qg.metadata:
|
|
222
|
+
graph_output_run = qg.metadata.get("output_run", output_run)
|
|
223
|
+
if graph_output_run != output_run:
|
|
224
|
+
raise ValueError(
|
|
225
|
+
f"Output run defined on command line ({output_run}) has to be "
|
|
226
|
+
f"identical to graph metadata ({graph_output_run}). "
|
|
227
|
+
"To update graph metadata run `pipetask update-graph-run` command."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Make sure that --extend-run always enables --skip-existing,
|
|
231
|
+
# clobbering should be disabled if --extend-run is not specified.
|
|
232
|
+
if extend_run:
|
|
233
|
+
skip_existing = True
|
|
234
|
+
else:
|
|
235
|
+
clobber_outputs = False
|
|
236
|
+
|
|
237
|
+
# Make butler instance. QuantumGraph should have an output run defined,
|
|
238
|
+
# but we ignore it here and let command line decide actual output run.
|
|
239
|
+
butler = ButlerFactory.make_write_butler(
|
|
240
|
+
butler_config,
|
|
241
|
+
qg.pipeline_graph,
|
|
212
242
|
output=output,
|
|
213
243
|
output_run=output_run,
|
|
244
|
+
inputs=inputs,
|
|
214
245
|
extend_run=extend_run,
|
|
246
|
+
rebase=rebase,
|
|
215
247
|
replace_run=replace_run,
|
|
216
248
|
prune_replaced=prune_replaced,
|
|
217
|
-
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
assert butler.run is not None, "Guaranteed by make_write_butler."
|
|
252
|
+
if skip_existing:
|
|
253
|
+
skip_existing_in += (butler.run,)
|
|
254
|
+
|
|
255
|
+
# Enable lsstDebug debugging. Note that this is done once in the
|
|
256
|
+
# main process before PreExecInit and it is also repeated before
|
|
257
|
+
# running each task in SingleQuantumExecutor (which may not be
|
|
258
|
+
# needed if `multiprocessing` always uses fork start method).
|
|
259
|
+
if enable_lsst_debug:
|
|
260
|
+
try:
|
|
261
|
+
_LOG.debug("Will try to import debug.py")
|
|
262
|
+
import debug # type: ignore # noqa: F401
|
|
263
|
+
except ImportError:
|
|
264
|
+
_LOG.warning("No 'debug' module found.")
|
|
265
|
+
|
|
266
|
+
# Save all InitOutputs, configs, etc.
|
|
267
|
+
if register_dataset_types:
|
|
268
|
+
qg.pipeline_graph.register_dataset_types(butler, include_packages=not no_versions)
|
|
269
|
+
if not skip_init_writes:
|
|
270
|
+
qg.write_init_outputs(butler, skip_existing=skip_existing)
|
|
271
|
+
qg.write_configs(butler, compare_existing=extend_run)
|
|
272
|
+
if not no_versions:
|
|
273
|
+
qg.write_packages(butler, compare_existing=extend_run)
|
|
274
|
+
|
|
275
|
+
if init_only:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
if task_factory is None:
|
|
279
|
+
task_factory = TaskFactory()
|
|
280
|
+
resources = ExecutionResources(
|
|
281
|
+
num_cores=cores_per_quantum, max_mem=memory_per_quantum, default_mem_units=u.MB
|
|
282
|
+
)
|
|
283
|
+
quantum_executor = SingleQuantumExecutor(
|
|
284
|
+
butler=butler,
|
|
285
|
+
task_factory=task_factory,
|
|
218
286
|
skip_existing_in=skip_existing_in,
|
|
219
|
-
skip_existing=skip_existing,
|
|
220
|
-
enableLsstDebug=debug,
|
|
221
|
-
fail_fast=fail_fast,
|
|
222
287
|
clobber_outputs=clobber_outputs,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
enable_implicit_threading=enable_implicit_threading,
|
|
226
|
-
cores_per_quantum=cores_per_quantum,
|
|
227
|
-
memory_per_quantum=memory_per_quantum,
|
|
228
|
-
rebase=rebase,
|
|
288
|
+
enable_lsst_debug=enable_lsst_debug,
|
|
289
|
+
resources=resources,
|
|
229
290
|
raise_on_partial_outputs=raise_on_partial_outputs,
|
|
230
291
|
)
|
|
231
292
|
|
|
232
|
-
|
|
233
|
-
|
|
293
|
+
if timeout is None:
|
|
294
|
+
timeout = MP_TIMEOUT
|
|
295
|
+
executor = MPGraphExecutor(
|
|
296
|
+
num_proc=processes,
|
|
297
|
+
timeout=timeout,
|
|
298
|
+
start_method=start_method,
|
|
299
|
+
quantum_executor=quantum_executor,
|
|
300
|
+
fail_fast=fail_fast,
|
|
301
|
+
pdb=pdb,
|
|
302
|
+
execution_graph_fixup=_import_graph_fixup(graph_fixup),
|
|
303
|
+
)
|
|
304
|
+
# Have to reset connection pool to avoid sharing connections with
|
|
305
|
+
# forked processes.
|
|
306
|
+
butler.registry.resetConnectionPool()
|
|
307
|
+
try:
|
|
308
|
+
with lsst.utils.timer.profile(profile, _LOG):
|
|
309
|
+
executor.execute(qg)
|
|
310
|
+
finally:
|
|
311
|
+
if summary:
|
|
312
|
+
report = executor.getReport()
|
|
313
|
+
if report:
|
|
314
|
+
with ResourcePath(summary).open("w") as out:
|
|
315
|
+
# Do not save fields that are not set.
|
|
316
|
+
out.write(report.model_dump_json(exclude_none=True, indent=2))
|
|
234
317
|
|
|
235
|
-
# If we have no output run specified, use the one from the graph rather
|
|
236
|
-
# than letting a new timestamped run be created.
|
|
237
|
-
if not args.output_run and qgraphObj.metadata and (output_run := qgraphObj.metadata.get("output_run")):
|
|
238
|
-
args.output_run = output_run
|
|
239
318
|
|
|
240
|
-
|
|
319
|
+
def _import_graph_fixup(graph_fixup: str) -> ExecutionGraphFixup | None:
|
|
320
|
+
"""Import/instantiate graph fixup object.
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
graph_fixup : `str`
|
|
325
|
+
Graph fixup command-line argument.
|
|
326
|
+
|
|
327
|
+
Returns
|
|
328
|
+
-------
|
|
329
|
+
fixup : `ExecutionGraphFixup` or `None`
|
|
330
|
+
Object that imposes additional ordering constraints on the graph.
|
|
331
|
+
|
|
332
|
+
Raises
|
|
333
|
+
------
|
|
334
|
+
ValueError
|
|
335
|
+
Raised if import fails, method call raises exception, or returned
|
|
336
|
+
instance has unexpected type.
|
|
337
|
+
"""
|
|
338
|
+
from lsst.pipe.base.execution_graph_fixup import ExecutionGraphFixup
|
|
339
|
+
|
|
340
|
+
if graph_fixup:
|
|
341
|
+
try:
|
|
342
|
+
factory = doImportType(graph_fixup)
|
|
343
|
+
except Exception as exc:
|
|
344
|
+
raise ValueError("Failed to import graph fixup class/method") from exc
|
|
345
|
+
try:
|
|
346
|
+
fixup = factory()
|
|
347
|
+
except Exception as exc:
|
|
348
|
+
raise ValueError("Failed to make instance of graph fixup") from exc
|
|
349
|
+
if not isinstance(fixup, ExecutionGraphFixup):
|
|
350
|
+
raise ValueError("Graph fixup is not an instance of ExecutionGraphFixup class")
|
|
351
|
+
return fixup
|
|
352
|
+
return None
|
|
@@ -25,36 +25,59 @@
|
|
|
25
25
|
# You should have received a copy of the GNU General Public License
|
|
26
26
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
27
27
|
|
|
28
|
-
import
|
|
29
|
-
from types import SimpleNamespace
|
|
28
|
+
from __future__ import annotations
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
import pickle
|
|
31
|
+
import uuid
|
|
32
|
+
from collections.abc import Mapping
|
|
33
|
+
from typing import Literal
|
|
34
|
+
|
|
35
|
+
import astropy.units as u
|
|
36
|
+
|
|
37
|
+
import lsst.utils.timer
|
|
38
|
+
from lsst.daf.butler import (
|
|
39
|
+
DatasetType,
|
|
40
|
+
DimensionConfig,
|
|
41
|
+
DimensionUniverse,
|
|
42
|
+
LimitedButler,
|
|
43
|
+
Quantum,
|
|
44
|
+
QuantumBackedButler,
|
|
45
|
+
)
|
|
46
|
+
from lsst.pipe.base import BuildId, ExecutionResources, QuantumGraph, TaskFactory
|
|
47
|
+
from lsst.pipe.base.mp_graph_executor import MPGraphExecutor
|
|
48
|
+
from lsst.pipe.base.single_quantum_executor import SingleQuantumExecutor
|
|
49
|
+
from lsst.resources import ResourcePath, ResourcePathExpression
|
|
50
|
+
from lsst.utils.logging import VERBOSE, getLogger
|
|
32
51
|
from lsst.utils.threads import disable_implicit_threading
|
|
33
52
|
|
|
34
|
-
from
|
|
53
|
+
from ..butler_factory import ButlerFactory
|
|
54
|
+
from ..utils import MP_TIMEOUT, summarize_quantum_graph
|
|
35
55
|
|
|
36
|
-
|
|
56
|
+
_LOG = getLogger(__name__)
|
|
37
57
|
|
|
38
58
|
|
|
39
59
|
def run_qbb(
|
|
40
|
-
|
|
41
|
-
|
|
60
|
+
*,
|
|
61
|
+
task_factory: TaskFactory | None = None,
|
|
62
|
+
butler_config: ResourcePathExpression,
|
|
63
|
+
qgraph: ResourcePathExpression,
|
|
42
64
|
config_search_path: list[str] | None,
|
|
43
65
|
qgraph_id: str | None,
|
|
44
|
-
qgraph_node_id: list[
|
|
66
|
+
qgraph_node_id: list[str | uuid.UUID] | None,
|
|
45
67
|
processes: int,
|
|
46
68
|
pdb: str | None,
|
|
47
69
|
profile: str | None,
|
|
48
70
|
debug: bool,
|
|
49
|
-
start_method:
|
|
71
|
+
start_method: Literal["spawn", "forkserver"] | None,
|
|
50
72
|
timeout: int | None,
|
|
51
73
|
fail_fast: bool,
|
|
52
|
-
summary:
|
|
74
|
+
summary: ResourcePathExpression | None,
|
|
53
75
|
enable_implicit_threading: bool,
|
|
54
76
|
cores_per_quantum: int,
|
|
55
77
|
memory_per_quantum: str,
|
|
56
78
|
raise_on_partial_outputs: bool,
|
|
57
79
|
no_existing_outputs: bool,
|
|
80
|
+
**kwargs: object,
|
|
58
81
|
) -> None:
|
|
59
82
|
"""Implement the command line interface ``pipetask run-qbb`` subcommand.
|
|
60
83
|
|
|
@@ -63,6 +86,8 @@ def run_qbb(
|
|
|
63
86
|
|
|
64
87
|
Parameters
|
|
65
88
|
----------
|
|
89
|
+
task_factory : `lsst.pipe.base.TaskFactory`, optional
|
|
90
|
+
A custom task factory to use.
|
|
66
91
|
butler_config : `str`
|
|
67
92
|
The path location of the gen3 butler/registry config file.
|
|
68
93
|
qgraph : `str`
|
|
@@ -109,36 +134,146 @@ def run_qbb(
|
|
|
109
134
|
no_existing_outputs : `bool`
|
|
110
135
|
Whether to assume that no predicted outputs for these quanta already
|
|
111
136
|
exist in the output run collection.
|
|
137
|
+
**kwargs : `object`
|
|
138
|
+
Ignored; click commands may accept options for more than one script
|
|
139
|
+
function and pass all the option kwargs to each of the script functions
|
|
140
|
+
which ignore these unused kwargs.
|
|
112
141
|
"""
|
|
113
142
|
# Fork option still exists for compatibility but we use spawn instead.
|
|
114
|
-
if start_method == "fork":
|
|
115
|
-
start_method = "spawn"
|
|
116
|
-
|
|
143
|
+
if start_method == "fork": # type: ignore[comparison-overlap]
|
|
144
|
+
start_method = "spawn" # type: ignore[unreachable]
|
|
145
|
+
_LOG.warning("Option --start-method=fork is unsafe and no longer supported, using spawn instead.")
|
|
117
146
|
|
|
118
147
|
if not enable_implicit_threading:
|
|
119
148
|
disable_implicit_threading()
|
|
120
149
|
|
|
121
|
-
|
|
150
|
+
# Load quantum graph.
|
|
151
|
+
nodes = qgraph_node_id or None
|
|
152
|
+
with lsst.utils.timer.time_this(
|
|
153
|
+
_LOG,
|
|
154
|
+
msg=f"Reading {str(len(nodes)) if nodes is not None else 'all'} quanta.",
|
|
155
|
+
level=VERBOSE,
|
|
156
|
+
) as qg_read_time:
|
|
157
|
+
qg = QuantumGraph.loadUri(
|
|
158
|
+
qgraph, nodes=nodes, graphID=BuildId(qgraph_id) if qgraph_id is not None else None
|
|
159
|
+
)
|
|
160
|
+
job_metadata = {"qg_read_time": qg_read_time.duration, "qg_size": len(qg)}
|
|
161
|
+
|
|
162
|
+
if qg.metadata is None:
|
|
163
|
+
raise ValueError("QuantumGraph is missing metadata, cannot continue.")
|
|
164
|
+
|
|
165
|
+
summarize_quantum_graph(qg)
|
|
166
|
+
|
|
167
|
+
dataset_types = {dstype.name: dstype for dstype in qg.registryDatasetTypes()}
|
|
168
|
+
|
|
169
|
+
# Ensure that QBB uses shared datastore cache.
|
|
170
|
+
ButlerFactory.define_datastore_cache()
|
|
171
|
+
|
|
172
|
+
_butler_factory = _QBBFactory(
|
|
122
173
|
butler_config=butler_config,
|
|
123
|
-
|
|
174
|
+
dimensions=qg.universe,
|
|
175
|
+
dataset_types=dataset_types,
|
|
124
176
|
config_search_path=config_search_path,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# make special quantum executor
|
|
180
|
+
resources = ExecutionResources(
|
|
181
|
+
num_cores=cores_per_quantum, max_mem=memory_per_quantum, default_mem_units=u.MB
|
|
182
|
+
)
|
|
183
|
+
quantumExecutor = SingleQuantumExecutor(
|
|
184
|
+
butler=None,
|
|
185
|
+
task_factory=task_factory,
|
|
186
|
+
enable_lsst_debug=debug,
|
|
187
|
+
limited_butler_factory=_butler_factory,
|
|
188
|
+
resources=resources,
|
|
189
|
+
assume_no_existing_outputs=no_existing_outputs,
|
|
190
|
+
skip_existing=True,
|
|
191
|
+
clobber_outputs=True,
|
|
192
|
+
raise_on_partial_outputs=raise_on_partial_outputs,
|
|
193
|
+
job_metadata=job_metadata,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
timeout = MP_TIMEOUT if timeout is None else timeout
|
|
197
|
+
executor = MPGraphExecutor(
|
|
198
|
+
num_proc=processes,
|
|
132
199
|
timeout=timeout,
|
|
200
|
+
start_method=start_method,
|
|
201
|
+
quantum_executor=quantumExecutor,
|
|
133
202
|
fail_fast=fail_fast,
|
|
134
|
-
|
|
135
|
-
enable_implicit_threading=enable_implicit_threading,
|
|
136
|
-
cores_per_quantum=cores_per_quantum,
|
|
137
|
-
memory_per_quantum=memory_per_quantum,
|
|
138
|
-
raise_on_partial_outputs=raise_on_partial_outputs,
|
|
139
|
-
no_existing_outputs=no_existing_outputs,
|
|
203
|
+
pdb=pdb,
|
|
140
204
|
)
|
|
205
|
+
try:
|
|
206
|
+
with lsst.utils.timer.profile(profile, _LOG):
|
|
207
|
+
executor.execute(qg)
|
|
208
|
+
finally:
|
|
209
|
+
if summary:
|
|
210
|
+
report = executor.getReport()
|
|
211
|
+
if report:
|
|
212
|
+
with ResourcePath(summary).open("w") as out:
|
|
213
|
+
# Do not save fields that are not set.
|
|
214
|
+
out.write(report.model_dump_json(exclude_none=True, indent=2))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class _QBBFactory:
|
|
218
|
+
"""Class which is a callable for making QBB instances.
|
|
219
|
+
|
|
220
|
+
This class is also responsible for reconstructing correct dimension
|
|
221
|
+
universe after unpickling. When pickling multiple things that require
|
|
222
|
+
dimension universe, this class must be unpickled first. The logic in
|
|
223
|
+
MPGraphExecutor ensures that SingleQuantumExecutor is unpickled first in
|
|
224
|
+
the subprocess, which causes unpickling of this class.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
def __init__(
|
|
228
|
+
self,
|
|
229
|
+
butler_config: ResourcePathExpression,
|
|
230
|
+
dimensions: DimensionUniverse,
|
|
231
|
+
dataset_types: Mapping[str, DatasetType],
|
|
232
|
+
config_search_path: list[str] | None,
|
|
233
|
+
):
|
|
234
|
+
self.butler_config = butler_config
|
|
235
|
+
self.dimensions = dimensions
|
|
236
|
+
self.dataset_types = dataset_types
|
|
237
|
+
self.config_search_path = config_search_path
|
|
238
|
+
|
|
239
|
+
def __call__(self, quantum: Quantum) -> LimitedButler:
|
|
240
|
+
"""Return freshly initialized `~lsst.daf.butler.QuantumBackedButler`.
|
|
241
|
+
|
|
242
|
+
Factory method to create QuantumBackedButler instances.
|
|
243
|
+
"""
|
|
244
|
+
return QuantumBackedButler.initialize(
|
|
245
|
+
config=self.butler_config,
|
|
246
|
+
quantum=quantum,
|
|
247
|
+
dimensions=self.dimensions,
|
|
248
|
+
dataset_types=self.dataset_types,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def _unpickle(
|
|
253
|
+
cls,
|
|
254
|
+
butler_config: ResourcePathExpression,
|
|
255
|
+
dimensions_config: DimensionConfig | None,
|
|
256
|
+
dataset_types_pickle: bytes,
|
|
257
|
+
config_search_path: list[str] | None,
|
|
258
|
+
) -> _QBBFactory:
|
|
259
|
+
universe = DimensionUniverse(dimensions_config)
|
|
260
|
+
dataset_types = pickle.loads(dataset_types_pickle)
|
|
261
|
+
return _QBBFactory(butler_config, universe, dataset_types, config_search_path)
|
|
141
262
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
263
|
+
def __reduce__(self) -> tuple:
|
|
264
|
+
# If dimension universe is not default one, we need to dump/restore
|
|
265
|
+
# its config.
|
|
266
|
+
config = self.dimensions.dimensionConfig
|
|
267
|
+
default = DimensionConfig()
|
|
268
|
+
# Only send configuration to other side if it is non-default, default
|
|
269
|
+
# will be instantiated from config=None.
|
|
270
|
+
if (config["namespace"], config["version"]) != (default["namespace"], default["version"]):
|
|
271
|
+
dimension_config = config
|
|
272
|
+
else:
|
|
273
|
+
dimension_config = None
|
|
274
|
+
# Dataset types need to be unpickled only after universe is made.
|
|
275
|
+
dataset_types_pickle = pickle.dumps(self.dataset_types)
|
|
276
|
+
return (
|
|
277
|
+
self._unpickle,
|
|
278
|
+
(self.butler_config, dimension_config, dataset_types_pickle, self.config_search_path),
|
|
279
|
+
)
|