GitHub user jrt1899 created a discussion: Parallelism bottleneck in multi-UDF 
workflows

**The problem**

In multiUDF Python workflows - UDF1 (n workers) → UDF2 (m workers), the most 
performance gains come from increasing worker parallelism. But increasing 
parallelism on both stages can leave downstream workers idle for a long time, 
limiting the gains.

The cause: each UDF1 worker creates one independent buffer per UDF2 worker, 
filled round-robin. With n UDF1 workers and m UDF2 workers, that's n × m 
independent buffers total. Each buffer only ships once it reaches the 
configured Data Transfer Batch (DTB) size, so in the worst case, the link needs 
roughly n × m × DTB tuples produced before any data reaches UDF2.

Since this threshold scales with n × m, adding more workers to either stage 
makes idle time worse, not better — parallelism ends up widening the gap before 
data starts flowing downstream instead of shortening the job.

```
UDF1 worker A ──┬─[DTB]──> UDF2 worker 1
                ├─[DTB]──> UDF2 worker 2
                └─ … m separate buffers

UDF1 worker B ──┬─[DTB]──> UDF2 worker 1   ← a different buffer
                ├─[DTB]──> UDF2 worker 2
                └─ … m separate buffers
...
UDF1 worker n ──┬─[DTB]──> UDF2 worker 1   ← a different buffer
                ├─[DTB]──> UDF2 worker 2
                └─ … m separate buffers
```
                
In our test workflow (CSV scan → UDF → UDF, 8 workers per stage), n = m = 8 and 
DTB = 5,000 — meaning ~320,000 rows had to accumulate before anything moved 
downstream.

**Design 1: Threshold-triggered early flush**

Texera already polls each worker roughly twice a second for live execution 
stats. We extended that poll to also track unshipped data per buffer and force 
an early release once it exceeds the configured threshold. This caused the two 
UDF stages to overlap, cutting our test workload's (two-filter credit card 
fraud detection) latency by 39.5% in the CPU-only environment.

This is a simple change, but it has a flaw: forcing an early release means 
shipped batches are smaller than the configured DTB value, so what's configured 
no longer matches what actually happens.

**Design 2: Least-loaded routing**

This addresses the root cause instead of patching around it, and preserves the 
configured DTB. Instead of one buffer per (UDF1 worker, UDF2 worker) pair, each 
UDF1 worker holds a single buffer. Once it fills to the configured DTB size, it 
ships to whichever UDF2 worker currently has the smallest backlog (tuples 
delivered but not yet processed, from the same poll).

This removes the n × m multiplier entirely — a UDF1 worker only needs to fill 
one DTB-sized batch before its first shipment goes out, and routing adapts 
automatically as the two stages' relative speeds shift during the run. This 
brought the latency improvement of 34.3% compared to the baseline in the same 
environment.

Code: https://github.com/jrt1899/texera/tree/least-loaded-routing

GitHub link: https://github.com/apache/texera/discussions/8519

----
This is an automatically sent email for [email protected].
To unsubscribe, please send an email to: [email protected]

Reply via email to