Skip to content

[pipeline] Remove fuse_subprocess_stages #1588

Draft
mthrok wants to merge 1 commit into
mainfrom
to5
Draft

[pipeline] Remove fuse_subprocess_stages #1588
mthrok wants to merge 1 commit into
mainfrom
to5

Conversation

@mthrok

@mthrok mthrok commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to the .to() region API (1/4–4/4; see #1584 for the overall design and rationale). Removes the executor-identity fusion path now that .to() regions provide the same capability with an explicit, statically-configurable surface. Both removed features were added in the still-unreleased 0.6.0 cycle, so no deprecation shim is warranted.

Removed:

  • The fuse_subprocess_stages keyword from PipelineBuilder.build, build_pipeline, and run_pipeline_in_subprocess.
  • The executor-identity fusion machinery in _fuse.py (_find_fusable_runs, _scan_run, _FusableRun, _fusable_*, _fuse_subprocess_stages, _pool_params, the identity _build_fused_stage) and the async-op-as-fusion-tag pass (_strip_async_executor_tags). The marker path (_fuse_marked_regions and friends) is kept.

Reverted (the async-op executor relaxation from #1582): an async op may no longer be given an executor. PipeConfig.__post_init__ again rejects any executor on an async op, and convert_to_async asserts it is None. In a .to() region an async op is placed by the marker and needs no per-stage executor tag, so the relaxation is obsolete.

run_pipeline_in_subprocess now fuses .to() regions in the main process (via _fuse_marked_regions), preserving the main-ownership of region worker pools that the old flag provided.

Docs: the parallelism guide's "Multi-processing (fused)" section is rewritten as "Multi-processing (region)" using .to().

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 6, 2026
@meta-codesync

meta-codesync Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

This pull request has been imported. If you are a Meta employee, you can view this in D110766675. (Because this pull request was imported automatically, there will not be any future comments.)

moto-meta pushed a commit that referenced this pull request Jul 10, 2026
This is part 1/4 of a series (plus a follow-up) adding the `.to()` region API to the SPDL `PipelineBuilder` — a declarative way to designate where a run of pipeline stages executes: the main process, a subprocess pool, or a subinterpreter pool. This diff carries the overview for the whole series; the later diffs point back here for the rationale.

Running stages off the main process previously meant attaching a shared `ProcessPoolExecutor` instance to consecutive `pipe()` calls and passing `fuse_subprocess_stages=True` to `build()`. The engine fused adjacent stages that shared the same executor object into one subprocess pipeline, so the value handed from one stage to the next stayed in the worker instead of being pickled back to the main process between stages. That worked but had real limitations: it was implicit (it keyed off executor object identity, which is easy to get wrong and cannot be expressed as static config), it required a live `Executor` (so a pipeline could not be fully described as serializable data), it could not pull `aggregate`/`disaggregate`/`path_variants` stages into a region (a fusion run was bounded at those stages), and it had no subinterpreter support.

The `.to()` API makes the region explicit and declarative. `to(ProcessPoolExecutorConfig(...))` or `to(InterpreterPoolExecutorConfig(...))` opens a region; every stage after it runs in that worker pool until `to(MAIN_PROCESS)` closes the region. The target is a serializable spec rather than a live executor, so a pipeline stays expressible as static config; a region may contain `aggregate`/`disaggregate`/`path_variants`; the worker-pool configuration has a single home; and subinterpreter pools (Python 3.14+) are supported alongside subprocess pools. The value passed between stages inside a region never leaves the worker and need not be picklable — only the region's inputs and outputs cross the boundary.

- 1/4 — this PR (#1584): the config-layer building blocks (the `ExecutorConfig` spec hierarchy — `ProcessPoolExecutorConfig` and `InterpreterPoolExecutorConfig` — plus `MAIN_PROCESS` and the `PlacementConfig` region marker), and widening `PipelineConfig.pipes` to carry region markers. Additive and inert.
- 2/4 — #1585: the engine pass `_fuse_marked_regions` that consumes the markers, replacing each maximal span of stages under a region target with one stage that runs the span as a nested pipeline in a worker pool. Dormant until markers exist.
- 3/4 — #1586: the subinterpreter worker-pool backend, behind a `_PoolBackend` seam, so a region can run in subinterpreters as well as subprocesses.
- 4/4 — #1587: the public surface (`PipelineBuilder.to()` and its validation), which makes the feature usable end to end.
- follow-up — #1588 (BC-breaking): removes the superseded `fuse_subprocess_stages` executor-identity fusion path now that `.to()` provides the same capability with an explicit, statically-configurable surface.

Adds the config-layer types, additive only — no behavior change yet:

- `ExecutorConfig`: the base spec for a worker-pool executor — a serializable description of a pool (worker count, initializer, initargs) that the pipeline later materializes into a live executor. Kept as a base so more executor targets (e.g. a thread pool) can be added without touching the placement machinery, and so the specs can be reused wherever a pool is configured.
- `ProcessPoolExecutorConfig` / `InterpreterPoolExecutorConfig`: the two concrete `ExecutorConfig` subclasses, for a subprocess or subinterpreter worker-pool region. The process variant adds `mp_context`; the subinterpreter variant adds nothing (no new process is started). They describe the pool without holding a live `Executor`.
- `MAIN_PROCESS`: a sentinel target that closes a region and brings subsequent stages back to the main process. It reprs as `MAIN_PROCESS` and, unlike `None`, is unambiguous and serializes cleanly.
- `PlacementConfig`: a region-marker node carried in `PipelineConfig.pipes`; stages after a marker (until the next one) run on its target (an `ExecutorConfig` or `MAIN_PROCESS`).

`PipelineConfig.pipes` now accepts `PlacementConfig`. Nothing produces these markers yet (`PipelineBuilder.to()` lands in 4/4), so this is inert; the fusion pass that consumes them lands in 2/4.

Note: a region's worker thread count and stats interval are pipeline/build settings, not properties of a placement, so they are not fields of these specs — the engine derives the region's thread count from the concurrency of the stages it fuses and inherits the stats interval from `build_pipeline`.
@moto-meta moto-meta force-pushed the to5 branch 2 times, most recently from e2c13b6 to 09a92c0 Compare July 10, 2026 18:17
moto-meta pushed a commit that referenced this pull request Jul 10, 2026
This is part 1/4 of a series (plus a follow-up) adding the `.to()` region API to the SPDL `PipelineBuilder` — a declarative way to designate where a run of pipeline stages executes: the main process, a subprocess pool, or a subinterpreter pool. This diff carries the overview for the whole series; the later diffs point back here for the rationale.

Running stages off the main process previously meant attaching a shared `ProcessPoolExecutor` instance to consecutive `pipe()` calls and passing `fuse_subprocess_stages=True` to `build()`. The engine fused adjacent stages that shared the same executor object into one subprocess pipeline, so the value handed from one stage to the next stayed in the worker instead of being pickled back to the main process between stages. That worked but had real limitations: it was implicit (it keyed off executor object identity, which is easy to get wrong and cannot be expressed as static config), it required a live `Executor` (so a pipeline could not be fully described as serializable data), it could not pull `aggregate`/`disaggregate`/`path_variants` stages into a region (a fusion run was bounded at those stages), and it had no subinterpreter support.

The `.to()` API makes the region explicit and declarative. `to(ProcessPoolExecutorConfig(...))` or `to(InterpreterPoolExecutorConfig(...))` opens a region; every stage after it runs in that worker pool until `to(MAIN_PROCESS)` closes the region. The target is a serializable spec rather than a live executor, so a pipeline stays expressible as static config; a region may contain `aggregate`/`disaggregate`/`path_variants`; the worker-pool configuration has a single home; and subinterpreter pools (Python 3.14+) are supported alongside subprocess pools. The value passed between stages inside a region never leaves the worker and need not be picklable — only the region's inputs and outputs cross the boundary.

- 1/4 — this PR (#1584): the config-layer building blocks (the `ExecutorConfig` spec hierarchy — `ProcessPoolExecutorConfig` and `InterpreterPoolExecutorConfig` — plus `MAIN_PROCESS` and the `PlacementConfig` region marker), and widening `PipelineConfig.pipes` to carry region markers. Additive and inert.
- 2/4 — #1585: the engine pass `_fuse_marked_regions` that consumes the markers, replacing each maximal span of stages under a region target with one stage that runs the span as a nested pipeline in a worker pool. Dormant until markers exist.
- 3/4 — #1586: the subinterpreter worker-pool backend, behind a `_PoolBackend` seam, so a region can run in subinterpreters as well as subprocesses.
- 4/4 — #1587: the public surface (`PipelineBuilder.to()` and its validation), which makes the feature usable end to end.
- follow-up — #1588 (BC-breaking): removes the superseded `fuse_subprocess_stages` executor-identity fusion path now that `.to()` provides the same capability with an explicit, statically-configurable surface.

Adds the config-layer types, additive only — no behavior change yet:

- `ExecutorConfig`: the base spec for a worker-pool executor — a serializable description of a pool (worker count, initializer, initargs) that the pipeline later materializes into a live executor. Kept as a base so more executor targets (e.g. a thread pool) can be added without touching the placement machinery, and so the specs can be reused wherever a pool is configured.
- `ProcessPoolExecutorConfig` / `InterpreterPoolExecutorConfig`: the two concrete `ExecutorConfig` subclasses, for a subprocess or subinterpreter worker-pool region. The process variant adds `mp_context`; the subinterpreter variant adds nothing (no new process is started). They describe the pool without holding a live `Executor`.
- `MAIN_PROCESS`: a sentinel target that closes a region and brings subsequent stages back to the main process. It reprs as `MAIN_PROCESS` and, unlike `None`, is unambiguous and serializes cleanly.
- `PlacementConfig`: a region-marker node carried in `PipelineConfig.pipes`; stages after a marker (until the next one) run on its target (an `ExecutorConfig` or `MAIN_PROCESS`).

`PipelineConfig.pipes` now accepts `PlacementConfig`. Nothing produces these markers yet (`PipelineBuilder.to()` lands in 4/4), so this is inert; the fusion pass that consumes them lands in 2/4.

Note: a region's worker thread count and stats interval are pipeline/build settings, not properties of a placement, so they are not fields of these specs — the engine derives the region's thread count from the concurrency of the stages it fuses and inherits the stats interval from `build_pipeline`.
@moto-meta moto-meta force-pushed the to5 branch 2 times, most recently from 5f46b05 to d03499a Compare July 10, 2026 19:45
moto-meta pushed a commit that referenced this pull request Jul 10, 2026
This is part 1/4 of a series (plus a follow-up) adding the `.to()` region API to the SPDL `PipelineBuilder` — a declarative way to designate where a run of pipeline stages executes: the main process, a subprocess pool, or a subinterpreter pool. This diff carries the overview for the whole series; the later diffs point back here for the rationale.

Running stages off the main process previously meant attaching a shared `ProcessPoolExecutor` instance to consecutive `pipe()` calls and passing `fuse_subprocess_stages=True` to `build()`. The engine fused adjacent stages that shared the same executor object into one subprocess pipeline, so the value handed from one stage to the next stayed in the worker instead of being pickled back to the main process between stages. That worked but had real limitations: it was implicit (it keyed off executor object identity, which is easy to get wrong and cannot be expressed as static config), it required a live `Executor` (so a pipeline could not be fully described as serializable data), it could not pull `aggregate`/`disaggregate`/`path_variants` stages into a region (a fusion run was bounded at those stages), and it had no subinterpreter support.

The `.to()` API makes the region explicit and declarative. `to(ProcessPoolExecutorConfig(...))` or `to(InterpreterPoolExecutorConfig(...))` opens a region; every stage after it runs in that worker pool until `to(MAIN_PROCESS)` closes the region. The target is a serializable spec rather than a live executor, so a pipeline stays expressible as static config; a region may contain `aggregate`/`disaggregate`/`path_variants`; the worker-pool configuration has a single home; and subinterpreter pools (Python 3.14+) are supported alongside subprocess pools. The value passed between stages inside a region never leaves the worker and need not be picklable — only the region's inputs and outputs cross the boundary.

- 1/4 — this PR (#1584): the config-layer building blocks (the `ExecutorConfig` spec hierarchy — `ProcessPoolExecutorConfig` and `InterpreterPoolExecutorConfig` — plus `MAIN_PROCESS` and the `PlacementConfig` region marker), and widening `PipelineConfig.pipes` to carry region markers. Additive and inert.
- 2/4 — #1585: the engine pass `_fuse_marked_regions` that consumes the markers, replacing each maximal span of stages under a region target with one stage that runs the span as a nested pipeline in a worker pool. Dormant until markers exist.
- 3/4 — #1586: the subinterpreter worker-pool backend, behind a `_PoolBackend` seam, so a region can run in subinterpreters as well as subprocesses.
- 4/4 — #1587: the public surface (`PipelineBuilder.to()` and its validation), which makes the feature usable end to end.
- follow-up — #1588 (BC-breaking): removes the superseded `fuse_subprocess_stages` executor-identity fusion path now that `.to()` provides the same capability with an explicit, statically-configurable surface.

Adds the config-layer types, additive only — no behavior change yet:

- `ExecutorConfig`: the base spec for a worker-pool executor — a serializable description of a pool (worker count, initializer, initargs) that the pipeline later materializes into a live executor. Kept as a base so more executor targets (e.g. a thread pool) can be added without touching the placement machinery, and so the specs can be reused wherever a pool is configured.
- `ProcessPoolExecutorConfig` / `InterpreterPoolExecutorConfig`: the two concrete `ExecutorConfig` subclasses, for a subprocess or subinterpreter worker-pool region. The process variant adds `mp_context`; the subinterpreter variant adds nothing (no new process is started). They describe the pool without holding a live `Executor`.
- `MAIN_PROCESS`: a sentinel target that closes a region and brings subsequent stages back to the main process. It reprs as `MAIN_PROCESS` and, unlike `None`, is unambiguous and serializes cleanly.
- `PlacementConfig`: a region-marker node carried in `PipelineConfig.pipes`; stages after a marker (until the next one) run on its target (an `ExecutorConfig` or `MAIN_PROCESS`).

`PipelineConfig.pipes` now accepts `PlacementConfig`. Nothing produces these markers yet (`PipelineBuilder.to()` lands in 4/4), so this is inert; the fusion pass that consumes them lands in 2/4.

Note: a region's worker thread count and stats interval are pipeline/build settings, not properties of a placement, so they are not fields of these specs — the engine derives the region's thread count from the concurrency of the stages it fuses and inherits the stats interval from `build_pipeline`.
mthrok added a commit that referenced this pull request Jul 10, 2026
This is part 1/4 of a series (plus a follow-up) adding the `.to()` region API to the SPDL `PipelineBuilder` — a declarative way to designate where a run of pipeline stages executes: the main process, a subprocess pool, or a subinterpreter pool. This diff carries the overview for the whole series; the later diffs point back here for the rationale.

Running stages off the main process previously meant attaching a shared `ProcessPoolExecutor` instance to consecutive `pipe()` calls and passing `fuse_subprocess_stages=True` to `build()`. The engine fused adjacent stages that shared the same executor object into one subprocess pipeline, so the value handed from one stage to the next stayed in the worker instead of being pickled back to the main process between stages. That worked but had real limitations: it was implicit (it keyed off executor object identity, which is easy to get wrong and cannot be expressed as static config), it required a live `Executor` (so a pipeline could not be fully described as serializable data), it could not pull `aggregate`/`disaggregate`/`path_variants` stages into a region (a fusion run was bounded at those stages), and it had no subinterpreter support.

The `.to()` API makes the region explicit and declarative. `to(ProcessPoolExecutorConfig(...))` or `to(InterpreterPoolExecutorConfig(...))` opens a region; every stage after it runs in that worker pool until `to(MAIN_PROCESS)` closes the region. The target is a serializable spec rather than a live executor, so a pipeline stays expressible as static config; a region may contain `aggregate`/`disaggregate`/`path_variants`; the worker-pool configuration has a single home; and subinterpreter pools (Python 3.14+) are supported alongside subprocess pools. The value passed between stages inside a region never leaves the worker and need not be picklable — only the region's inputs and outputs cross the boundary.

- 1/4 — this PR (#1584): the config-layer building blocks (the `ExecutorConfig` spec hierarchy — `ProcessPoolExecutorConfig` and `InterpreterPoolExecutorConfig` — plus `MAIN_PROCESS` and the `PlacementConfig` region marker), and widening `PipelineConfig.pipes` to carry region markers. Additive and inert.
- 2/4 — #1585: the engine pass `_fuse_marked_regions` that consumes the markers, replacing each maximal span of stages under a region target with one stage that runs the span as a nested pipeline in a worker pool. Dormant until markers exist.
- 3/4 — #1586: the subinterpreter worker-pool backend, behind a `_PoolBackend` seam, so a region can run in subinterpreters as well as subprocesses.
- 4/4 — #1587: the public surface (`PipelineBuilder.to()` and its validation), which makes the feature usable end to end.
- follow-up — #1588 (BC-breaking): removes the superseded `fuse_subprocess_stages` executor-identity fusion path now that `.to()` provides the same capability with an explicit, statically-configurable surface.

Adds the config-layer types, additive only — no behavior change yet:

- `ExecutorConfig`: the base spec for a worker-pool executor — a serializable description of a pool (worker count, initializer, initargs) that the pipeline later materializes into a live executor. Kept as a base so more executor targets (e.g. a thread pool) can be added without touching the placement machinery, and so the specs can be reused wherever a pool is configured.
- `ProcessPoolExecutorConfig` / `InterpreterPoolExecutorConfig`: the two concrete `ExecutorConfig` subclasses, for a subprocess or subinterpreter worker-pool region. The process variant adds `mp_context`; the subinterpreter variant adds nothing (no new process is started). They describe the pool without holding a live `Executor`.
- `MAIN_PROCESS`: a sentinel target that closes a region and brings subsequent stages back to the main process. It reprs as `MAIN_PROCESS` and, unlike `None`, is unambiguous and serializes cleanly.
- `PlacementConfig`: a region-marker node carried in `PipelineConfig.pipes`; stages after a marker (until the next one) run on its target (an `ExecutorConfig` or `MAIN_PROCESS`).

`PipelineConfig.pipes` now accepts `PlacementConfig`. Nothing produces these markers yet (`PipelineBuilder.to()` lands in 4/4), so this is inert; the fusion pass that consumes them lands in 2/4.

Note: a region's worker thread count and stats interval are pipeline/build settings, not properties of a placement, so they are not fields of these specs — the engine derives the region's thread count from the concurrency of the stages it fuses and inherits the stats interval from `build_pipeline`.
@mthrok mthrok force-pushed the to5 branch 2 times, most recently from ee07c3b to d94a3cc Compare July 10, 2026 22:15
Follow-up to the `.to()` region API (1/4–4/4; see #1584 for the overall design and rationale). Removes the executor-identity fusion path now that `.to()` regions provide the same capability with an explicit, statically-configurable surface. Both removed features were added in the still-unreleased 0.6.0 cycle, so no deprecation shim is warranted.

Removed:
- The `fuse_subprocess_stages` keyword from `PipelineBuilder.build`, `build_pipeline`, and `run_pipeline_in_subprocess`.
- The executor-identity fusion machinery in `_fuse.py` (`_find_fusable_runs`, `_scan_run`, `_FusableRun`, `_fusable_*`, `_fuse_subprocess_stages`, `_pool_params`, the identity `_build_fused_stage`) and the async-op-as-fusion-tag pass (`_strip_async_executor_tags`). The marker path (`_fuse_marked_regions` and friends) is kept.

Reverted (the async-op executor relaxation from #1582): an async op may no longer be given an `executor`. `PipeConfig.__post_init__` again rejects any executor on an async op, and `convert_to_async` asserts it is `None`. In a `.to()` region an async op is placed by the marker and needs no per-stage executor tag, so the relaxation is obsolete.

`run_pipeline_in_subprocess` now fuses `.to()` regions in the main process (via `_fuse_marked_regions`), preserving the main-ownership of region worker pools that the old flag provided.

Docs: the parallelism guide's "Multi-processing (fused)" section is rewritten as "Multi-processing (region)" using `.to()`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant