Skip to content

13. Staged pipelines

Runnable companion: examples/10_nested_pipeline_compare.py

The last chapter. Nothing new is introduced — this is how the previous eleven compose into a real multi-stage pipeline: an upstream model labels the data, a downstream model trains on those labels and emits an intermediate signal, and two final decoding options are scored against ground truth.

base dataset ──▶ model 1 ──attach teacher.cls──▶ base store
                            workset pins teacher.cls = model1
                       model 2 (trains on a workset) ──attach score.cls──▶ base store
                       add(score.cls, label.cls) into its own store
                                              final store
                                    ┌────────────────┴────────────────┐
                              optA ─attach decision.cls          optB ─attach decision.cls
                                    └──── accuracy ── compare ────┘

Chaining producers

Each hop is chapter 6 with chapter 9's pin in front of it:

# model 2's training workset consumes model 1's output explicitly
workset = client.create_workset(
    sources=[SourceSpec(sample_store="base", keys=["audio.wav", "teacher.cls"],
                        producer_pins={"teacher.cls": "model1"})],
    key_groups=[KeyGroupSpec(name="inputs", keys=["audio.wav"]),
                KeyGroupSpec(name="teacher", keys=["teacher.cls"])],
    shard_plan=ShardPlan(shard_size=64),
)
# … compute, then attach score.cls under producer "model2"

The pin is what makes the chain explicit: stage 2 consumes exactly the upstream producer it was built against, rather than whichever revision resolves.

Choosing a workset or a dataset per stage

This is the judgement call the chapter exists to demonstrate, and it is per stage rather than per pipeline:

Stage Source Why
Base model training dataset the run you will reproduce, publish and compare against
Follow-up stages workset cheap, live, iterated often; the exact membership is not the artifact
Final scoring dataset numbers you will quote need a frozen member set

Training on a workset is a legitimate choice rather than a shortcut: for an exploratory or intermediate stage, a frozen dataset buys nothing. Freeze exactly where reproducibility is a requirement.

Moving an intermediate into its own store

When a stage's output becomes the substrate for the next fan-out, add it across (chapter 5). A workset is a first-class add source:

client.create_store(name="final")
client.add_to_store(
    "final",
    operation_id="seed-from-stage2",
    from_workset=stage2_workset.workset_id,     # or from_store / from_dataset
    key_subset=["score.cls", "label.cls"],
)
client.wait_for_add("final", "seed-from-stage2", timeout_seconds=300)

In the new store, score.cls is an ordinary key on ordinary samples — no pin needed to read it, because the producer identity was resolved when the add was planned. That is what makes it a clean substrate for the next stage.

Fanning out options

Two decoding options are two producers writing the same key in one store — chapter 9's shape:

for option in ("optA", "optB"):
    ...  # attach decision.cls under producer `option`

# compare: one workset per option, each pinning its producer, scored vs the label

Then score each with a pinned workset and compare aggregates (chapter 12). An unpinned read cannot separate them: two producers, one key name.

Assembling this for real

One thing lives outside this guide because it is about where your code runs rather than about Datavo's model:

  • Producing and training on one machine — skipping the round trip via local shard IO: local shards.

And the recurring rule for a pipeline that will run more than once: every stage is idempotent by construction if you let it be. An attach workset drains itself, an import and an add replay on operation_id. That is the two mechanisms — convergence and replay — working together: pass stable ids and a re-run costs only the work that is genuinely missing. See idempotency.

Pitfalls

  • A shared producer id across stages. Two stages under one id cannot be compared, pinned apart, or removed separately. One producer per stage per configuration.
  • Changing what a producer computes. A producer's output keys are recorded from its attaches, so adding a key under the same id is fine — both are recorded, and coverage spans both. But new values for a key you already wrote are not: convergence skips samples that producer has covered. Changed weights or settings mean a new producer id.
  • Chaining without pins. Stage 2 then reads whichever revision resolves, which is a silent correctness bug the day a third producer appears.
  • Freezing every stage. Datasets are permanent and named; converting at every hop accumulates artifacts that nobody owns.

That is the complete model. From here: the reference for CLI, caching and integration, and the internals for how it works underneath.