3535 TaskHook ,
3636)
3737from spdl .pipeline ._executor_proxy import _make_config_executors_picklable
38- from spdl .pipeline ._fuse import (
39- _fuse_marked_regions ,
40- _fuse_subprocess_stages ,
41- _strip_async_executor_tags ,
42- )
38+ from spdl .pipeline ._fuse import _fuse_marked_regions
4339from spdl .pipeline ._iter_utils import iterate_in_subinterpreter , iterate_in_subprocess
4440from spdl .pipeline ._random_seed import _capture_rng_initializers
4541from spdl .pipeline ._subprocess_pipeline_pool import _shutdown_pipeline_pools
@@ -142,39 +138,20 @@ def _build_pipeline(
142138 stage_id : int = 0 ,
143139 background_tasks : list [BackgroundTaskFactory ] | None = None ,
144140 use_thread_output_queue : bool = False ,
145- fuse_subprocess_stages : bool = False ,
146141) -> Pipeline [U ]:
147142 if _DEFAULT_BUILD_CALLBACK is not None :
148143 try :
149144 _DEFAULT_BUILD_CALLBACK (pipeline_cfg )
150145 except Exception :
151146 _LG .exception ("Build callback failed." )
152147
153- pools : list [Any ] = []
154- # Both fusion passes eagerly spawn worker pools. Reap them together on failure: each pass
155- # only reaps its own pools if it raises, so without this a failure in the second pass would
156- # leak the pools the first already spawned -- this half-built pipeline is never returned to
157- # the caller to be stopped.
158- try :
159- # Honor explicit `.to()` region markers first. This is a no-op when the config has no
160- # markers, so it is always safe to run and independent of `fuse_subprocess_stages`.
161- # stacklevel=4: _fuse_marked_regions -> _build_pipeline -> build_pipeline -> user.
162- pipeline_cfg , region_pools = _fuse_marked_regions (
163- pipeline_cfg , report_stats_interval = report_stats_interval , stacklevel = 4
164- )
165- pools .extend (region_pools )
166- if fuse_subprocess_stages :
167- # Fuse consecutive same-pool stages so each run executes as one nested pipeline
168- # inside a worker pool, eliminating the inter-stage IPC. The pools are owned by the
169- # returned Pipeline and reaped when it stops.
170- # stacklevel=4: _fuse_subprocess_stages -> _build_pipeline -> build_pipeline -> user.
171- pipeline_cfg , id_pools = _fuse_subprocess_stages (
172- pipeline_cfg , report_stats_interval = report_stats_interval , stacklevel = 4
173- )
174- pools .extend (id_pools )
175- except BaseException :
176- _shutdown_pipeline_pools (pools )
177- raise
148+ # Fuse each `.to()` region into one nested-pipeline stage that runs in a worker pool,
149+ # eliminating the inter-stage IPC within the region. A no-op when the config has no markers.
150+ # The pools are owned by the returned Pipeline and reaped when it stops.
151+ # stacklevel=4: _fuse_marked_regions -> _build_pipeline -> build_pipeline -> user.
152+ pipeline_cfg , pools = _fuse_marked_regions (
153+ pipeline_cfg , report_stats_interval = report_stats_interval , stacklevel = 4
154+ )
178155
179156 desc = repr (pipeline_cfg )
180157
@@ -218,7 +195,6 @@ def build_pipeline(
218195 stage_id : int = 0 ,
219196 background_tasks : list [BackgroundTaskFactory ] | None = None ,
220197 use_thread_output_queue : bool = False ,
221- fuse_subprocess_stages : bool = False ,
222198) -> Pipeline [U ]:
223199 """Build a pipeline from the config.
224200
@@ -291,24 +267,11 @@ def build_pipeline(
291267 ``asyncio.run_coroutine_threadsafe``, reducing per-batch latency from
292268 ~200-400us to ~10us. Default: ``False``.
293269
294- fuse_subprocess_stages: If ``True``, fuse runs of two or more adjacent pipe stages that
295- share the same process-pool (or interpreter-pool) executor instance into a single
296- stage that executes the run as one nested pipeline inside a worker pool. This
297- eliminates the inter-stage IPC that otherwise round-trips data back to this process
298- between each stage (so intermediate values need not be picklable), while each fused
299- stage keeps its own ``concurrency`` and per-stage stats. A ``path_variants`` stage
300- whose branches all use the same pool executor is fused too — the whole routing
301- construct (router and branches) moves into the worker — and fuses on its own even
302- when it is the only such stage. An ``aggregate``/``disaggregate`` between two pool
303- stages is not fused (it keeps its main-process batching) and splits them into
304- separate runs. An async op joins a fused run when tagged with the same executor as
305- its neighbours (see :py:meth:`~spdl.pipeline.PipelineBuilder.pipe`), running on the
306- worker's own event loop. Continuous sources are supported (the fused worker
307- sub-pipelines stay warm across epochs and epoch boundaries are propagated across the
308- pool). Default: ``False``.
309-
310- .. versionadded:: 0.6.0
311- The ``fuse_subprocess_stages`` argument.
270+ .. seealso::
271+
272+ :py:meth:`~spdl.pipeline.PipelineBuilder.to`
273+ Designate a region of stages to run together in a subprocess (or subinterpreter)
274+ worker pool, eliminating the inter-stage IPC within the region.
312275 """
313276 from . import _profile
314277
@@ -325,7 +288,6 @@ def build_pipeline(
325288 stage_id = stage_id ,
326289 background_tasks = background_tasks ,
327290 use_thread_output_queue = use_thread_output_queue ,
328- fuse_subprocess_stages = fuse_subprocess_stages ,
329291 )
330292
331293
@@ -427,7 +389,6 @@ def run_pipeline_in_subprocess(
427389 task_hook_factory : Callable [[StageInfo ], list [TaskHook ]] | None = None ,
428390 background_tasks : list [BackgroundTaskFactory ] | None = None ,
429391 use_thread_output_queue : bool = False ,
430- fuse_subprocess_stages : bool = False ,
431392 ** kwargs : Any ,
432393) -> Iterable [T ]:
433394 """Run the given Pipeline in a subprocess, and iterate on the result.
@@ -579,22 +540,16 @@ def run_pipeline_in_subprocess(
579540
580541 num_threads,max_failures,report_stats_interval,queue_class,task_hook_factory,background_tasks:
581542 Passed to :py:func:`build_pipeline`.
582- fuse_subprocess_stages: If ``True``, fuse runs of two or more adjacent pipe stages that
583- share the same process-pool (or interpreter-pool) executor instance into a single
584- stage that runs the run as one nested pipeline inside a worker pool. The worker
585- processes are spawned in (and owned by) the main process, exactly like a hoisted
586- ``ProcessPoolExecutor``; the pipeline subprocess drives them through a queue handle.
587- This removes the per-stage round-trip between the pipeline subprocess and the pool
588- workers (so intermediate values need not be picklable). A ``path_variants`` stage
589- whose branches all use the same pool executor is fused too (router and branches move
590- into the worker). An async op joins a fused run when tagged with the same executor as
591- its neighbours (see :py:meth:`~spdl.pipeline.PipelineBuilder.pipe`), running on the
592- worker's own event loop. Continuous sources are supported. Default: ``False``.
593-
594- .. versionadded:: 0.6.0
595- The ``fuse_subprocess_stages`` argument.
596543 kwargs: Passed to :py:func:`iterate_in_subprocess`.
597544
545+ .. seealso::
546+
547+ :py:meth:`~spdl.pipeline.PipelineBuilder.to`
548+ Designate a region of stages to run together in a subprocess (or subinterpreter)
549+ worker pool. When the config has such a region, its worker pool is spawned in (and
550+ owned by) the main process, so it is not orphaned if the pipeline subprocess is
551+ force-killed.
552+
598553 Yields:
599554 The results yielded from the pipeline.
600555
@@ -625,11 +580,11 @@ def run_pipeline_in_subprocess(
625580 else config_or_builder .get_config () # pyre-ignore[16]
626581 )
627582
628- # Every pass below eagerly spawns worker pools, so they all run inside one try/except:
629- # ``_fuse_marked_regions`` spawns ``fuse_pools`` up front, and if any * later* pass
630- # (``_fuse_subprocess_stages``, `` _hoist_process_pools``) or the iterable creation
631- # raises, both ``fuse_pools`` and the hoisted ``pools`` must be reaped -- this half-built
632- # iterable is never returned to the caller to be stopped. Mirrors ``_build_pipeline``.
583+ # Both passes below eagerly spawn worker pools, so they run inside one try/except:
584+ # ``_fuse_marked_regions`` spawns ``fuse_pools`` up front, so if a later pass
585+ # (``_hoist_process_pools``) or the iterable creation raises, both ``fuse_pools`` and the
586+ # hoisted ``pools`` must be reaped -- this half-built iterable is never returned to the
587+ # caller to be stopped. Mirrors the guard in ``_build_pipeline``.
633588 fuse_pools : list [Any ] = []
634589 pools : list [Any ] = []
635590 try :
@@ -648,23 +603,6 @@ def run_pipeline_in_subprocess(
648603 stacklevel = 3 ,
649604 )
650605 fuse_pools .extend (region_pools )
651- # Also fuse runs of same-pool stages tagged with an identical executor into one nested
652- # pipeline inside a worker pool, eliminating the inter-stage IPC (before hoisting, so
653- # only unfused ProcessPoolExecutor stages remain).
654- if fuse_subprocess_stages :
655- # stacklevel=3: _fuse_subprocess_stages -> run_pipeline_in_subprocess -> user.
656- config , id_pools = _fuse_subprocess_stages (
657- config ,
658- mp_context = kwargs .get ("mp_context" ),
659- report_stats_interval = report_stats_interval ,
660- stacklevel = 3 ,
661- )
662- fuse_pools .extend (id_pools )
663-
664- # Clear executor tags left on any unfused async op: they are subprocess fusion-group
665- # hints, not real pools, and the executor-hoisting/pickling passes below are op-agnostic
666- # -- an async op's process-pool tag would otherwise spawn an idle pool it never uses.
667- config = _strip_async_executor_tags (config )
668606
669607 # Spawn workers for any stdlib ``ProcessPoolExecutor`` in the main process (as children
670608 # of main, not grandchildren via the pipeline subprocess), then replace the executor
@@ -693,9 +631,9 @@ def run_pipeline_in_subprocess(
693631 ** kwargs ,
694632 )
695633 except BaseException :
696- # Any eager-spawn pass above (region fusion, identity fusion, hoisting) or the iterable
697- # creation failed; the iterable is never returned to the caller, so reap the region and
698- # hoisted pools here to avoid leaking their worker processes and pipe fds.
634+ # Any eager-spawn pass above (region fusion, hoisting) or the iterable creation failed;
635+ # the iterable is never returned to the caller, so reap the region and hoisted pools
636+ # here to avoid leaking their worker processes and pipe fds.
699637 _shutdown_pools (pools )
700638 _shutdown_pipeline_pools (fuse_pools )
701639 raise
0 commit comments