William RodriguezBypassing the GIL in Data Pipelines: Parallel DAG Execution in Wpipe Day 11 of the...
Day 11 of the Wisrovi Open Source Architecture Series.
When executing complex Directed Acyclic Graphs (DAGs) in Python, traditional orchestrators often hit the wall of the Global Interpreter Lock (GIL) when running CPU-intensive transformation steps, or suffer from massive memory overhead when spinning up heavy separate containers for trivial parallel branches.
wpipe solves this through a hybrid execution engine: it automatically executes I/O-bound steps asynchronously via asyncio worker threads, while dispatching heavy mathematical computations to dedicated worker processes with zero-copy shared memory context.
| Architectural Dimension | Heavy Orchestrators (Airflow/Prefect) |
wpipe Hybrid Engine |
|---|---|---|
| Worker Allocation | Separate Docker container / Celery worker | Lightweight in-process or sub-process worker |
| Branch Scheduling | Inter-service polling via message broker | Direct topological DAG dependency resolution |
| Inter-Step Communication | Network serialization (S3/XCom/JSON) | In-memory atomic Context / SQLite WAL |
| Startup Latency | Seconds to minutes | < 5 milliseconds |
| Memory Footprint | Gigabytes per worker node | Megabytes (Sovereign local machine) |
Here is how you construct and run a parallel branch pipeline where tasks execute concurrently without boilerplate multiprocessing locks:
from wpipe import Pipeline, Step, Context
class FetchSourceA(Step):
def run(self, ctx: Context) -> None:
# Simulating external data fetch
ctx.set("data_a", [x * 2 for x in range(10000)])
print("Source A fetched.")
class FetchSourceB(Step):
def run(self, ctx: Context) -> None:
# Simulating independent data fetch
ctx.set("data_b", [x * 3 for x in range(10000)])
print("Source B fetched.")
class MergeAndCalculate(Step):
def run(self, ctx: Context) -> None:
# Depends on both Source A and Source B
a = ctx.get("data_a")
b = ctx.get("data_b")
total_sum = sum(a) + sum(b)
ctx.set("total_sum", total_sum)
print(f"Aggregated Result: {total_sum}")
# Initialize pipeline with parallel branch resolution
pipeline = Pipeline("ParallelIngestionEngine", max_workers=4)
# Define step graph: Source A and B run concurrently
step_a = FetchSourceA()
step_b = FetchSourceB()
merge = MergeAndCalculate()
pipeline.add_step(step_a)
pipeline.add_step(step_b)
# Merge step runs automatically once both A and B complete
pipeline.add_step(merge, depends_on=[step_a, step_b])
result = pipeline.execute()
print("Execution Status:", result.status)
print("Pipeline Context Output:", result.context.get("total_sum"))