Pipeline definitions¶
Comprehensive example defining a complex pipeline with spdl.pipeline.defs.
This example showcases the usage of all configuration classes available in the
spdl.pipeline.defs module, including:
SourceConfig: Configures data sources for pipelinesPipeConfig: Configures individual processing stages (via factory functions)SinkConfig: Configures output buffering for pipelinesPipelineConfig: Top-level pipeline configuration combining all componentsMerge: Merges outputs from multiple pipelines into a single streamPathVariantsConfig: Routes items to different processing paths
The example also demonstrates the factory functions:
Pipe(): Creates pipe configurations for general processingAggregate(): Creates configurations for batching/grouping dataDisaggregate(): Creates configurations for splitting batched dataPathVariants(): Creates variant path routing configurations
Note
This pipeline uses the merge mechanism, which is not supported by
PipelineBuilder.
The data flow:
Pipeline 1:
[0, 1, 2, 3, 4]→square→[[0, 1], [4, 9], [16]]Pipeline 2:
[10, 11, 12, 13, 14]→add_100→[110, 111, 112, 113, 114]Merge combines outputs:
[[0, 1], [4, 9], [16], 110, 111, 112, 113, 114]Normalize handles mixed data types:
[[0, 1], [4, 9], [16], [110], [111], [112], [113], [114]]Disaggregate flattens:
[0, 1, 4, 9, 16, 110, 111, 112, 113, 114]PathVariants routes each item based on cache membership:
Cache hits (pre-populated values) →
load_from_cache(1 stage)Cache misses →
multiply_by_10→store_in_cache(2 stages: compute×10, then store the result for future lookups)
Source¶
Source
Click here to see the source.
1#!/usr/bin/env python3
2# Copyright (c) Meta Platforms, Inc. and affiliates.
3# All rights reserved.
4#
5# This source code is licensed under the BSD-style license found in the
6# LICENSE file in the root directory of this source tree.
7
8"""Comprehensive example defining a complex pipeline with :py:mod:`spdl.pipeline.defs`.
9
10This example showcases the usage of all configuration classes available in the
11:py:mod:`spdl.pipeline.defs` module, including:
12
13.. py:currentmodule:: spdl.pipeline.defs
14
15- :py:class:`SourceConfig`: Configures data sources for pipelines
16- :py:class:`PipeConfig`: Configures individual processing stages (via factory functions)
17- :py:class:`SinkConfig`: Configures output buffering for pipelines
18- :py:class:`PipelineConfig`: Top-level pipeline configuration combining all components
19- :py:class:`Merge`: Merges outputs from multiple pipelines into a single stream
20- :py:class:`PathVariantsConfig`: Routes items to different processing paths
21
22The example also demonstrates the factory functions:
23
24- :py:func:`Pipe`: Creates pipe configurations for general processing
25- :py:func:`Aggregate`: Creates configurations for batching/grouping data
26- :py:func:`Disaggregate`: Creates configurations for splitting batched data
27- :py:func:`PathVariants`: Creates variant path routing configurations
28
29.. note::
30
31 This pipeline uses the merge mechanism, which is not supported by
32 :py:class:`~spdl.pipeline.PipelineBuilder`.
33
34.. mermaid::
35
36 graph TD
37 subgraph "Pipeline 1"
38 S1[Source: range#40;5#41;] --> P1[Pipe: square]
39 P1 --> AGG1[Aggregate: batch_size=2]
40 AGG1 --> SNK1[Sink 1]
41 end
42
43 subgraph "Pipeline 2"
44 S2[Source: range#40;10,15#41;] --> P2[Pipe: add_100]
45 P2 --> SNK2[Sink 2]
46 end
47
48 subgraph "Main Pipeline"
49 SNK1 --> M[Merge]
50 SNK2 --> M
51 M --> NORM[Pipe: normalize_to_lists]
52 NORM --> DISAGG[Disaggregate]
53 DISAGG --> R[Router: cache_router]
54 R -->|cache hit| CACHE[Pipe: load_from_cache]
55 R -->|cache miss| MUL[Pipe: multiply_by_10]
56 MUL --> STORE[Pipe: store_in_cache]
57 CACHE --> PV_MERGE[PathVariants merge]
58 STORE --> PV_MERGE
59 PV_MERGE --> FINAL_SINK[Final Sink]
60 end
61
62The data flow:
63
641. Pipeline 1:
65 ``[0, 1, 2, 3, 4]`` → ``square`` → ``[[0, 1], [4, 9], [16]]``
662. Pipeline 2:
67 ``[10, 11, 12, 13, 14]`` → ``add_100`` → ``[110, 111, 112, 113, 114]``
683. Merge combines outputs:
69 ``[[0, 1], [4, 9], [16], 110, 111, 112, 113, 114]``
704. Normalize handles mixed data types:
71 ``[[0, 1], [4, 9], [16], [110], [111], [112], [113], [114]]``
725. Disaggregate flattens:
73 ``[0, 1, 4, 9, 16, 110, 111, 112, 113, 114]``
746. PathVariants routes each item based on cache membership:
75
76 - Cache hits (pre-populated values) → ``load_from_cache`` (1 stage)
77 - Cache misses → ``multiply_by_10`` → ``store_in_cache`` (2 stages:
78 compute ``×10``, then store the result for future lookups)
79"""
80
81__all__ = [
82 "main",
83 "create_sub_pipeline_1",
84 "create_sub_pipeline_2",
85 "create_main_pipeline",
86 "square",
87 "add_100",
88 "multiply_by_10",
89 "store_in_cache",
90 "normalize_to_lists",
91 "cache_router",
92 "load_from_cache",
93 "run_pipeline_example",
94]
95
96import logging
97from typing import Any
98
99from spdl.pipeline import build_pipeline
100from spdl.pipeline.defs import (
101 Aggregate,
102 Disaggregate,
103 Merge,
104 PathVariants,
105 Pipe,
106 PipelineConfig,
107 SinkConfig,
108 SourceConfig,
109)
110
111_LG: logging.Logger = logging.getLogger(__name__)
112
113
114def square(x: int) -> int:
115 """Square the input number."""
116 return x * x
117
118
119def add_100(x: int) -> int:
120 """Add 100 to the input number."""
121 return x + 100
122
123
124################################################################################
125# Cache-based routing with PathVariants
126################################################################################
127
128# Cache of pre-computed results. In a real pipeline this might be backed by
129# a key-value store or an on-disk cache; here we use a plain dict.
130_CACHE: dict[int, int] = {0: 0, 1: 10, 4: 40} # pre-populated with a few ×10 values
131
132
133def cache_router(item: int) -> int:
134 """Route items based on cache membership.
135
136 Returns 0 for cache hits (fast path) and 1 for cache misses (slow path).
137 """
138 return 0 if item in _CACHE else 1
139
140
141def load_from_cache(item: int) -> int:
142 """Retrieve a previously computed result from the cache (fast path)."""
143 return _CACHE[item]
144
145
146def multiply_by_10(item: int) -> tuple[int, int]:
147 """Multiply the input by 10 (slow path, first stage).
148
149 Returns a (key, result) tuple so the next stage can store it in the cache.
150 """
151 return (item, item * 10)
152
153
154def store_in_cache(item: tuple[int, int]) -> int:
155 """Store the computed result in the cache and return it (slow path, second stage)."""
156 key, value = item
157 _CACHE[key] = value
158 return value
159
160
161def create_sub_pipeline_1() -> PipelineConfig[list[int]]:
162 """Create a sub-pipeline that squares numbers and aggregates them.
163
164 .. code-block:: text
165
166 range(5)
167 → square
168 → aggregate(2)
169 → [[squared_pairs], [remaining]]
170
171 Returns:
172 Configuration for a pipeline that processes
173 ``[0,1,2,3,4]`` into batches of squared values.
174 """
175 source_config = SourceConfig(range(5))
176 square_pipe = Pipe(square)
177 aggregate_pipe = Aggregate(2, drop_last=False)
178 sink_config = SinkConfig(buffer_size=10)
179 return PipelineConfig(
180 src=source_config,
181 pipes=[square_pipe, aggregate_pipe],
182 sink=sink_config,
183 )
184
185
186def create_sub_pipeline_2() -> PipelineConfig[int]:
187 """Create a sub-pipeline that adds 100 to numbers.
188
189 .. code-block:: text
190
191 range(10,15)
192 → add_100
193 → individual_values
194
195 Returns:
196 Configuration for a pipeline that processes
197 ``[10,11,12,13,14]`` by adding ``100``.
198 """
199 source_config = SourceConfig(range(10, 15))
200 add_pipe = Pipe(add_100, concurrency=2)
201 sink_config = SinkConfig(buffer_size=5)
202
203 return PipelineConfig(
204 src=source_config,
205 pipes=[add_pipe],
206 sink=sink_config,
207 )
208
209
210def normalize_to_lists(item: Any) -> list[Any]:
211 """Flatten lists or wrap individual items in a list for uniform handling."""
212 if isinstance(item, list):
213 return item
214 else:
215 return [item]
216
217
218def create_main_pipeline(
219 sub_pipeline_1: PipelineConfig[list[int]],
220 sub_pipeline_2: PipelineConfig[int],
221) -> PipelineConfig[int]:
222 """Create the main pipeline that merges outputs from sub-pipelines.
223
224 After merging, normalising and disaggregating, the pipeline uses
225 :py:func:`PathVariants` to route each item through a cache-aware
226 multiply-by-10 stage:
227
228 - **Cache hit** → ``load_from_cache`` (1 pipe stage)
229 - **Cache miss** → ``multiply_by_10`` → ``store_in_cache`` (2 pipe stages)
230
231 Note that the two paths have different numbers of pipe stages,
232 which PathVariants supports.
233
234 .. code-block:: text
235
236 Merge([sub1, sub2])
237 → normalize_to_lists
238 → disaggregate
239 → PathVariants(cache_router)
240 path 0 (hit): load_from_cache
241 path 1 (miss): multiply_by_10 → store_in_cache
242
243 Args:
244 sub_pipeline_1: First sub-pipeline configuration
245 sub_pipeline_2: Second sub-pipeline configuration
246
247 Returns:
248 Main pipeline configuration that merges and processes the sub-pipeline outputs.
249 """
250 merge_config = Merge([sub_pipeline_1, sub_pipeline_2])
251 normalize_pipe = Pipe(normalize_to_lists)
252 disaggregate_pipe = Disaggregate()
253
254 # Instead of a plain Pipe(multiply_by_10), use PathVariants to
255 # demonstrate cache-based routing: items already in the cache are
256 # served instantly, while cache misses compute the result and
257 # populate the cache for future lookups.
258 cached_multiply = PathVariants(
259 router=cache_router,
260 paths=[
261 [Pipe(load_from_cache)], # path 0: 1-stage (fast)
262 [
263 Pipe(multiply_by_10),
264 Pipe(store_in_cache),
265 ], # path 1: 2-stage (compute + cache)
266 ],
267 )
268
269 sink_config = SinkConfig(buffer_size=20)
270
271 return PipelineConfig(
272 src=merge_config,
273 pipes=[normalize_pipe, disaggregate_pipe, cached_multiply],
274 sink=sink_config,
275 )
276
277
278def run_pipeline_example() -> list[int]:
279 """Execute the complete pipeline example and return results.
280
281 Returns:
282 List of processed integers from the merged pipeline execution.
283 """
284 _LG.info("Creating sub-pipeline configurations...")
285
286 sub_pipeline_1 = create_sub_pipeline_1()
287 sub_pipeline_2 = create_sub_pipeline_2()
288
289 _LG.info("Sub-pipeline 1: %s", sub_pipeline_1)
290 _LG.info("Sub-pipeline 2: %s", sub_pipeline_2)
291
292 main_pipeline_config = create_main_pipeline(sub_pipeline_1, sub_pipeline_2)
293
294 _LG.info("Main pipeline config: %s", main_pipeline_config)
295
296 _LG.info("Building the pipeline.")
297 pipeline = build_pipeline(main_pipeline_config, num_threads=4)
298
299 _LG.info("Executing the pipeline.")
300 results = []
301 with pipeline.auto_stop():
302 for item in pipeline:
303 results.append(item)
304
305 return results
306
307
308def run() -> None:
309 """Run example pipeline and check the result."""
310 results = run_pipeline_example()
311
312 _LG.info("Final results: %s", results)
313 _LG.info("Number of items processed: %d", len(results))
314
315 # Verify expected data flow
316 expected_squared = [0, 1, 4, 9, 16] # squares of 0-4
317 expected_added = [110, 111, 112, 113, 114] # 10-14 + 100
318 expected_combined_count = len(expected_squared) + len(expected_added)
319
320 if len(results) != expected_combined_count:
321 raise RuntimeError(
322 f"✗ Unexpected number of items: got {len(results)}, expected {expected_combined_count}"
323 )
324 _LG.info("✓ Pipeline processed expected number of items")
325
326
327def main() -> None:
328 """Main entry point demonstrating all pipeline configuration classes."""
329 logging.basicConfig(
330 level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s"
331 )
332
333 run()
334
335
336if __name__ == "__main__":
337 main()
API Reference¶
Functions
- create_sub_pipeline_1() PipelineConfig[list[int]][source]¶
Create a sub-pipeline that squares numbers and aggregates them.
range(5) → square → aggregate(2) → [[squared_pairs], [remaining]]
- Returns:
Configuration for a pipeline that processes
[0,1,2,3,4]into batches of squared values.
- create_sub_pipeline_2() PipelineConfig[int][source]¶
Create a sub-pipeline that adds 100 to numbers.
range(10,15) → add_100 → individual_values
- Returns:
Configuration for a pipeline that processes
[10,11,12,13,14]by adding100.
- create_main_pipeline(sub_pipeline_1: PipelineConfig[list[int]], sub_pipeline_2: PipelineConfig[int]) PipelineConfig[int][source]¶
Create the main pipeline that merges outputs from sub-pipelines.
After merging, normalising and disaggregating, the pipeline uses
PathVariants()to route each item through a cache-aware multiply-by-10 stage:Cache hit →
load_from_cache(1 pipe stage)Cache miss →
multiply_by_10→store_in_cache(2 pipe stages)
Note that the two paths have different numbers of pipe stages, which PathVariants supports.
Merge([sub1, sub2]) → normalize_to_lists → disaggregate → PathVariants(cache_router) path 0 (hit): load_from_cache path 1 (miss): multiply_by_10 → store_in_cache- Parameters:
sub_pipeline_1 – First sub-pipeline configuration
sub_pipeline_2 – Second sub-pipeline configuration
- Returns:
Main pipeline configuration that merges and processes the sub-pipeline outputs.
- multiply_by_10(item: int) tuple[int, int][source]¶
Multiply the input by 10 (slow path, first stage).
Returns a (key, result) tuple so the next stage can store it in the cache.
- store_in_cache(item: tuple[int, int]) int[source]¶
Store the computed result in the cache and return it (slow path, second stage).
- normalize_to_lists(item: Any) list[Any][source]¶
Flatten lists or wrap individual items in a list for uniform handling.
- cache_router(item: int) int[source]¶
Route items based on cache membership.
Returns 0 for cache hits (fast path) and 1 for cache misses (slow path).