Source code for dipy.workflows.stage_isolation

"""Run a workflow stage in a fresh subprocess.

Workflows built on PyTorch load the ``libomp`` bundled with the torch wheel.
When DIPY's OpenMP extensions have already loaded another copy of ``libomp``
in the same process, torch can crash while loading model weights. Running
such stages in a spawned subprocess keeps the two runtimes apart.

This module must stay free of heavy imports: the spawned child imports it to
find :func:`stage_worker`, and anything imported here is loaded in the child
before the workflow itself. :mod:`dipy.utils.logging` loads no compiled
extension.
"""

import importlib
import multiprocessing as mp
import os
from queue import Empty
import traceback

from dipy.utils.logging import add_file_handler, attached_log_files, logger

_POLL_SECONDS = 0.5


[docs] def stage_worker( *, queue, module_name, class_name, init_params, run_params, log_level, log_files ): """Run a workflow inside a spawned subprocess. The outcome is put on ``queue`` as a ``(status, payload)`` pair: ``("ok", last_generated_outputs)`` on success, ``("error", formatted_traceback)`` when the workflow raises. Parameters ---------- queue : multiprocessing.Queue Used to pass the outcome back to the parent. module_name : str Dotted module path of the workflow. class_name : str Class name of the workflow inside ``module_name``. init_params : dict Keyword arguments forwarded to ``workflow.__init__``. run_params : dict Keyword arguments forwarded to ``workflow.run``. log_level : int Level of the DIPY logger in the parent. log_files : list of tuple ``(filename, level)`` of the log files of the parent, which the child appends to. """ # Both copies are LLVM libomp, so letting a second one initialise is safe; # it must be allowed before the workflow import loads either of them. os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE") logger.setLevel(log_level) for handler in logger.handlers: handler.setLevel(log_level) for filename, level in log_files: add_file_handler(filename, level=level) try: module = importlib.import_module(module_name) workflow = getattr(module, class_name)(**init_params) workflow.run(**run_params) outcome = ("ok", getattr(workflow, "last_generated_outputs", None)) except BaseException: error = traceback.format_exc() logger.error(f"Isolated stage {module_name}.{class_name} failed:\n{error}") outcome = ("error", error) queue.put(outcome)
[docs] def execute_stage_isolated(*, module_name, class_name, init_params, run_params): """Run a workflow stage in a spawned subprocess. The child logs to the same files and at the same level as the parent. Parameters ---------- module_name : str Dotted module path of the workflow. class_name : str Class name of the workflow inside ``module_name``. init_params : dict Keyword arguments forwarded to ``workflow.__init__``. run_params : dict Keyword arguments forwarded to ``workflow.run``. Returns ------- object The value of ``workflow.last_generated_outputs`` from the child process, or ``None`` if the attribute was absent. Raises ------ RuntimeError If the workflow raises in the child, with the child traceback in the message, or if the child exits without reporting an outcome. """ ctx = mp.get_context("spawn") queue = ctx.Queue() proc = ctx.Process( target=stage_worker, kwargs={ "queue": queue, "module_name": module_name, "class_name": class_name, "init_params": init_params, "run_params": run_params, "log_level": logger.getEffectiveLevel(), "log_files": attached_log_files(), }, ) proc.start() # The result is read before joining: a child blocks on exit until the # queue is drained, so joining first deadlocks on large outputs. outcome = None while outcome is None: try: outcome = queue.get(timeout=_POLL_SECONDS) except Empty: if proc.is_alive(): continue try: outcome = queue.get(timeout=_POLL_SECONDS) except Empty: proc.join() raise RuntimeError( f"Isolated stage subprocess exited with code {proc.exitcode} " "without reporting a result" ) from None proc.join() status, payload = outcome if status == "error": raise RuntimeError(f"Isolated stage subprocess failed:\n{payload}") return payload