Coverage for python / lsst / pipe / base / separable_pipeline_executor.py: 36%

64 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-12 01:14 -0800

1# This file is part of pipe_base. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28 

29from __future__ import annotations 

30 

31__all__ = [ 

32 "SeparablePipelineExecutor", 

33] 

34 

35 

36import datetime 

37import getpass 

38import logging 

39from collections.abc import Iterable 

40from typing import Any 

41 

42import lsst.resources 

43from lsst.daf.butler import Butler 

44 

45from ._quantumContext import ExecutionResources 

46from .all_dimensions_quantum_graph_builder import AllDimensionsQuantumGraphBuilder 

47from .graph import QuantumGraph 

48from .mp_graph_executor import MPGraphExecutor 

49from .pipeline import Pipeline 

50from .quantum_graph import PredictedQuantumGraph 

51from .quantum_graph_builder import QuantumGraphBuilder 

52from .quantum_graph_executor import QuantumGraphExecutor 

53from .single_quantum_executor import SingleQuantumExecutor 

54from .taskFactory import TaskFactory 

55 

56_LOG = logging.getLogger(__name__) 

57 

58 

59class SeparablePipelineExecutor: 

60 """An executor that allows each step of pipeline execution to be 

61 run independently. 

62 

63 The executor can run any or all of the following steps: 

64 

65 * pre-execution initialization 

66 * pipeline building 

67 * quantum graph generation 

68 * quantum graph execution 

69 

70 Any of these steps can also be handed off to external code without 

71 compromising the remaining ones. 

72 

73 Parameters 

74 ---------- 

75 butler : `lsst.daf.butler.Butler` 

76 A Butler whose ``collections`` and ``run`` attributes contain the input 

77 and output collections to use for processing. 

78 clobber_output : `bool`, optional 

79 If set, the pipeline execution overwrites existing output files. 

80 Otherwise, any conflict between existing and new outputs is an error. 

81 skip_existing_in : iterable [`str`], optional 

82 If not empty, the pipeline execution searches the listed collections 

83 for existing outputs, and skips any quanta that have run to completion 

84 (or have no work to do). Otherwise, all tasks are attempted (subject to 

85 ``clobber_output``). 

86 task_factory : `.TaskFactory`, optional 

87 A custom task factory for use in pre-execution and execution. By 

88 default, a new instance of `.TaskFactory` is used. 

89 resources : `.ExecutionResources` 

90 The resources available to each quantum being executed. 

91 raise_on_partial_outputs : `bool`, optional 

92 If `True` raise exceptions chained by 

93 `.AnnotatedPartialOutputsError` immediately, instead of 

94 considering the partial result a success and continuing to run 

95 downstream tasks. 

96 """ 

97 

98 def __init__( 

99 self, 

100 butler: Butler, 

101 clobber_output: bool = False, 

102 skip_existing_in: Iterable[str] | None = None, 

103 task_factory: TaskFactory | None = None, 

104 resources: ExecutionResources | None = None, 

105 raise_on_partial_outputs: bool = True, 

106 ): 

107 self._butler = Butler.from_config( 

108 butler=butler, collections=butler.collections.defaults, run=butler.run 

109 ) 

110 if not self._butler.collections.defaults: 

111 raise ValueError("Butler must specify input collections for pipeline.") 

112 if not self._butler.run: 

113 raise ValueError("Butler must specify output run for pipeline.") 

114 

115 self._clobber_output = clobber_output 

116 self._skip_existing_in = list(skip_existing_in) if skip_existing_in else [] 

117 

118 self._task_factory = task_factory if task_factory else TaskFactory() 

119 self.resources = resources 

120 self.raise_on_partial_outputs = raise_on_partial_outputs 

121 

122 def pre_execute_qgraph( 

123 self, 

124 graph: QuantumGraph | PredictedQuantumGraph, 

125 register_dataset_types: bool = False, 

126 save_init_outputs: bool = True, 

127 save_versions: bool = True, 

128 ) -> None: 

129 """Run pre-execution initialization. 

130 

131 This method will be deprecated after DM-38041, to be replaced with a 

132 method that takes either a `.Pipeline` or a 

133 resolved `.pipeline_graph.PipelineGraph` instead of a `.QuantumGraph`. 

134 

135 Parameters 

136 ---------- 

137 graph : `.QuantumGraph` or `.quantum_graph.PredictedQuantumGraph` 

138 The quantum graph defining the pipeline and datasets to 

139 be initialized. 

140 register_dataset_types : `bool`, optional 

141 If `True`, register all output dataset types from the pipeline 

142 represented by ``graph``. 

143 save_init_outputs : `bool`, optional 

144 If `True`, create init-output datasets in this object's output run. 

145 save_versions : `bool`, optional 

146 If `True`, save a package versions dataset. 

147 """ 

148 if register_dataset_types: 

149 graph.pipeline_graph.register_dataset_types(self._butler, include_packages=save_versions) 

150 if save_init_outputs: 

151 graph.write_init_outputs(self._butler, skip_existing=(self._butler.run in self._skip_existing_in)) 

152 graph.write_configs(self._butler) 

153 if save_versions: 

154 graph.write_packages(self._butler) 

155 

156 def make_pipeline(self, pipeline_uri: str | lsst.resources.ResourcePath) -> Pipeline: 

157 """Build a pipeline from pipeline and configuration information. 

158 

159 Parameters 

160 ---------- 

161 pipeline_uri : `str` or `lsst.resources.ResourcePath` 

162 URI to a file containing a pipeline definition. A URI fragment may 

163 be used to specify a subset of the pipeline, as described in 

164 :ref:`pipeline-running-intro`. 

165 

166 Returns 

167 ------- 

168 pipeline : `.Pipeline` 

169 The fully-built pipeline. 

170 """ 

171 return Pipeline.from_uri(pipeline_uri) 

172 

173 def make_quantum_graph_builder( 

174 self, 

175 pipeline: Pipeline, 

176 where: str = "", 

177 *, 

178 builder_class: type[QuantumGraphBuilder] = AllDimensionsQuantumGraphBuilder, 

179 **kwargs: Any, 

180 ) -> QuantumGraphBuilder: 

181 """Initialize a quantum graph builder from a pipeline and input 

182 datasets. 

183 

184 Parameters 

185 ---------- 

186 pipeline : `.Pipeline` 

187 The pipeline for which to generate a quantum graph. 

188 where : `str`, optional 

189 A data ID query that constrains the quanta generated. Must not be 

190 provided if a custom ``builder_class`` is given and that class does 

191 not accept ``where`` as a construction argument. 

192 builder_class : `type` [ \ 

193 `.quantum_graph_builder.QuantumGraphBuilder` ], optional 

194 Quantum graph builder implementation. Ignored if ``builder`` is 

195 provided. 

196 **kwargs 

197 Additional keyword arguments are forwarded to ``builder_class`` 

198 when a quantum graph builder instance is constructed. All 

199 arguments accepted by the 

200 `~.quantum_graph_builder.QuantumGraphBuilder` base 

201 class are provided automatically (from explicit arguments to this 

202 method and executor attributes) and do not need to be included 

203 as keyword arguments. 

204 

205 Returns 

206 ------- 

207 builder : `.quantum_graph_builder.QuantumGraphBuilder` 

208 A quantum graph builder. 

209 """ 

210 if where: 

211 # Only pass 'where' if it's actually provided, since some 

212 # QuantumGraphBuilder subclasses may not accept it. 

213 kwargs["where"] = where 

214 return builder_class( 

215 pipeline.to_graph(), 

216 self._butler, 

217 skip_existing_in=self._skip_existing_in, 

218 clobber=self._clobber_output, 

219 **kwargs, 

220 ) 

221 

222 def make_quantum_graph( 

223 self, 

224 pipeline: Pipeline, 

225 where: str = "", 

226 *, 

227 builder_class: type[QuantumGraphBuilder] = AllDimensionsQuantumGraphBuilder, 

228 attach_datastore_records: bool = False, 

229 **kwargs: Any, 

230 ) -> QuantumGraph: 

231 """Build a quantum graph from a pipeline and input datasets. 

232 

233 This returns an instance of the old `.QuantumGraph` class. Use 

234 `build_quantum_graph` to construct a 

235 `.quantum_graph.PredictedQuantumGraph`. 

236 

237 Parameters 

238 ---------- 

239 pipeline : `.Pipeline` 

240 The pipeline for which to generate a quantum graph. 

241 where : `str`, optional 

242 A data ID query that constrains the quanta generated. Must not be 

243 provided if a custom ``builder_class`` is given and that class does 

244 not accept ``where`` as a construction argument. 

245 builder_class : `type` [ \ 

246 `.quantum_graph_builder.QuantumGraphBuilder` ], optional 

247 Quantum graph builder implementation. Ignored if ``builder`` is 

248 provided. 

249 attach_datastore_records : `bool`, optional 

250 Whether to attach datastore records. These are currently used only 

251 by `lsst.daf.butler.QuantumBackedButler`, which is not used by 

252 `SeparablePipelineExecutor` for execution. 

253 **kwargs 

254 Additional keyword arguments are forwarded to ``builder_class`` 

255 when a quantum graph builder instance is constructed. All 

256 arguments accepted by the 

257 `~.quantum_graph_builder.QuantumGraphBuilder` base 

258 class are provided automatically (from explicit arguments to this 

259 method and executor attributes) and do not need to be included 

260 as keyword arguments. 

261 

262 Returns 

263 ------- 

264 graph : `.QuantumGraph` 

265 The quantum graph for ``.Pipeline`` as run on the datasets 

266 identified by ``where``. 

267 

268 Notes 

269 ----- 

270 This method does no special handling of empty quantum graphs. If 

271 needed, clients can use `len` to test if the returned graph is empty. 

272 """ 

273 metadata = { 

274 "input": self._butler.collections.defaults, 

275 "output_run": self._butler.run, 

276 "skip_existing_in": self._skip_existing_in, 

277 "skip_existing": bool(self._skip_existing_in), 

278 "data_query": where, 

279 "user": getpass.getuser(), 

280 "time": str(datetime.datetime.now()), 

281 } 

282 qg_builder = self.make_quantum_graph_builder(pipeline, where, builder_class=builder_class, **kwargs) 

283 graph = qg_builder.build(metadata=metadata, attach_datastore_records=attach_datastore_records) 

284 _LOG.info( 

285 "QuantumGraph contains %d quanta for %d tasks, graph ID: %r", 

286 len(graph), 

287 len(graph.taskGraph), 

288 graph.graphID, 

289 ) 

290 return graph 

291 

292 def build_quantum_graph( 

293 self, 

294 pipeline: Pipeline, 

295 where: str = "", 

296 *, 

297 builder_class: type[QuantumGraphBuilder] = AllDimensionsQuantumGraphBuilder, 

298 attach_datastore_records: bool = False, 

299 **kwargs: Any, 

300 ) -> PredictedQuantumGraph: 

301 """Build a quantum graph from a pipeline and input datasets. 

302 

303 This returns an instance of the new 

304 `.quantum_graph.PredictedQuantumGraph` class. Use `make_quantum_graph` 

305 to construct a `.QuantumGraph`. 

306 

307 Parameters 

308 ---------- 

309 pipeline : `.Pipeline` 

310 The pipeline for which to generate a quantum graph. 

311 where : `str`, optional 

312 A data ID query that constrains the quanta generated. Must not be 

313 provided if a custom ``builder_class`` is given and that class does 

314 not accept ``where`` as a construction argument. 

315 builder_class : `type` [ \ 

316 `.quantum_graph_builder.QuantumGraphBuilder` ], optional 

317 Quantum graph builder implementation. Ignored if ``builder`` is 

318 provided. 

319 attach_datastore_records : `bool`, optional 

320 Whether to attach datastore records. These are currently used only 

321 by `lsst.daf.butler.QuantumBackedButler`, which is not used by 

322 `SeparablePipelineExecutor` for execution. 

323 **kwargs 

324 Additional keyword arguments are forwarded to ``builder_class`` 

325 when a quantum graph builder instance is constructed. All 

326 arguments accepted by the 

327 `~.quantum_graph_builder.QuantumGraphBuilder` base 

328 class are provided automatically (from explicit arguments to this 

329 method and executor attributes) and do not need to be included 

330 as keyword arguments. 

331 

332 Returns 

333 ------- 

334 graph : `.QuantumGraph` 

335 The quantum graph for ``.Pipeline`` as run on the datasets 

336 identified by ``where``. 

337 

338 Notes 

339 ----- 

340 This method does no special handling of empty quantum graphs. If 

341 needed, clients can use `len` to test if the returned graph is empty. 

342 """ 

343 metadata = { 

344 "skip_existing_in": self._skip_existing_in, 

345 "skip_existing": bool(self._skip_existing_in), 

346 "data_query": where, 

347 } 

348 qg_builder = self.make_quantum_graph_builder(pipeline, where, builder_class=builder_class, **kwargs) 

349 graph = qg_builder.finish( 

350 metadata=metadata, attach_datastore_records=attach_datastore_records 

351 ).assemble() 

352 _LOG.info( 

353 "PredictedQuantumGraph contains %d quanta for %d tasks.", 

354 len(graph), 

355 len(graph.quanta_by_task), 

356 ) 

357 return graph 

358 

359 def run_pipeline( 

360 self, 

361 graph: QuantumGraph | PredictedQuantumGraph, 

362 fail_fast: bool = False, 

363 graph_executor: QuantumGraphExecutor | None = None, 

364 num_proc: int = 1, 

365 ) -> None: 

366 """Run a pipeline in the form of a prepared quantum graph. 

367 

368 Pre-execution initialization must have already been run; 

369 see `pre_execute_qgraph`. 

370 

371 Parameters 

372 ---------- 

373 graph : `.QuantumGraph` or `.quantum_graph.PredictedQuantumGraph` 

374 The pipeline and datasets to execute. 

375 fail_fast : `bool`, optional 

376 If `True`, abort all execution if any task fails when 

377 running with multiple processes. Only used with the default graph 

378 executor). 

379 graph_executor : `.quantum_graph_executor.QuantumGraphExecutor`,\ 

380 optional 

381 A custom graph executor. By default, a new instance of 

382 `.mp_graph_executor.MPGraphExecutor` is used. 

383 num_proc : `int`, optional 

384 The number of processes that can be used to run the pipeline. The 

385 default value ensures that no subprocess is created. Only used with 

386 the default graph executor. 

387 """ 

388 if not graph_executor: 

389 quantum_executor = SingleQuantumExecutor( 

390 butler=self._butler, 

391 task_factory=self._task_factory, 

392 skip_existing_in=self._skip_existing_in, 

393 clobber_outputs=self._clobber_output, 

394 resources=self.resources, 

395 raise_on_partial_outputs=self.raise_on_partial_outputs, 

396 ) 

397 graph_executor = MPGraphExecutor( 

398 num_proc=num_proc, 

399 timeout=2_592_000.0, # In practice, timeout is never helpful; set to 30 days. 

400 quantum_executor=quantum_executor, 

401 fail_fast=fail_fast, 

402 ) 

403 # Have to reset connection pool to avoid sharing connections with 

404 # forked processes. 

405 self._butler.registry.resetConnectionPool() 

406 

407 graph_executor.execute(graph)