olympipe-rs
Parallel data pipeline library for Rust, inspired by olympipe.
Each stage runs in its own thread (or several threads for task(..., count)), communicating via bounded crossbeam channels (capacity 4 by default, matching olympipe). Back-pressure is automatic: a slow downstream stage will slow down upstream producers.
Use case: ML inference pipelines
A common bottleneck in machine learning workloads is keeping the GPU busy. olympipe-rs makes it straightforward to overlap CPU-bound preprocessing and postprocessing with GPU inference in separate threads:
use Pipeline;
new
.task // 4 CPU threads decode & normalise
.task // 1 GPU thread, always fed
.task // 4 CPU threads decode results
.for_each;
Because each stage runs independently and channels act as bounded buffers, the GPU thread is never starved waiting for preprocessing, and preprocessed batches don't pile up unboundedly in memory.
Quick start
[]
= "0.1"
use Pipeline;
let result = new
.filter
.task // 4 parallel workers
.limit
.collect;
API
Constructors
| Method | Description |
|---|---|
Pipeline::new(iter) |
Feed any IntoIterator into the pipeline |
.with_buffer(n) |
Override inter-stage channel capacity (default: 4) |
Stages (consume self, return a new Pipeline)
| Method | Description |
|---|---|
.task(f, count) |
Map 1→1; count parallel workers share the channel |
.task_or(f, on_error) |
Map with fallback — requires T: Clone |
.filter(pred) |
Keep items where pred(&item) is true |
.batch(n) |
Group into Vec<T> of at most n items |
.temporal_batch(duration) |
Group items arriving within duration of each other |
.explode(f) |
FlatMap: f(item) returns an iterator |
.split(f) |
Route to two branches; returns (Pipeline<A>, Pipeline<B>) |
.gather(others) |
Merge multiple pipelines into one |
.reduce(init, f) |
Fold to a single value, emitted as the sole output item |
.limit(n) |
Pass at most n items downstream |
.timeout(duration) |
Error (disconnect) if no item arrives within duration |
.debug() |
println! each item and forward it |
Terminal methods
| Method | Description |
|---|---|
.collect() |
Drain into Vec<T>, join all threads |
.for_each(f) |
Apply f to each item, join all threads |
.wait_for_completion() |
Drain and discard, join all threads |
Split / Gather
use Pipeline;
let = new.split;
// Both branches can be consumed sequentially (split uses unbounded channels
// internally so the router thread never blocks).
let mut e = evens.collect;
let mut o = odds.collect;
e.sort; o.sort;
assert_eq!;
assert_eq!;
let p1 = new;
let p2 = new;
let mut result = p1.gather.collect;
result.sort;
assert_eq!;
Error handling
Worker panics are caught via std::panic::catch_unwind, logged to stderr, and treated as a clean shutdown for that worker (the output channel is dropped, propagating disconnection downstream — equivalent to olympipe's default behavior).
For recoverable errors in a stage, use task_or:
new
.task_or
.collect; // [1, 3]
Design notes
| olympipe (Python) | olympipe-rs (Rust) |
|---|---|
multiprocessing.Process per stage |
std::thread::spawn per stage |
multiprocessing.Queue(maxsize=4) |
crossbeam_channel::bounded(4) |
count=N → N processes sharing queues |
count → N threads, cloned Receiver (MPMC) |
| Manager DAG for shutdown | Channel disconnect cascades naturally |
split → N processes, one queue each |
split → 1 router thread, unbounded outputs |
gather → N identity processes |
gather → N forwarder threads, shared Sender |