Pipelines as documents¶
A pipeline in mosaic is a document: a JSON recipe of steps and the references between them, validated against the real registries, resolved against a dataset into a plan, and then run.
That is a deliberate choice over the more obvious one — a pipeline as a live object —
and the two coexist. Pipeline holds feature classes and a CallbackStep wrapping
a live callable, so it has no wire form at all. A recipe has nothing but a wire form.
What the document form buys¶
It is portable. A recipe never carries a resolved run_id, because those are
dataset state, and never carries an entry list, because those are about one dataset.
So the same file runs over several. What is specific to one submission — which
entries, which pinned artifact, which resolved feature versions — lives in a
Request beside the recipe rather than inside it.
It can be checked before anything happens. Validation runs before the dataset is
touched and reports all of its problems at once. It decides from declarations alone,
with no dataset at all. An editor can therefore refuse a wire as it is drawn. Each
feature declares the entity level of its output. Each feature mosaic registers also
declares the columns it reads from its merged input (requires) and the columns of its
output (provides). A feature registered outside mosaic may declare neither. Validation
walks the recipe and works out, for each step, the columns its input is guaranteed to
contain and the columns it may contain. A required column absent from every input is a
problem, and the problem names the inputs providing it. A required column is a caution
when an input may contain it. The tracks may contain each column allowed by a schema.
They may also contain columns outside every schema and feature declaration, such as
SLEAP's poseP0. A column that an undeclared feature may produce is a caution too. A
caution does not block a run. A step whose feature does not declare its requirements is
listed as unchecked, and the columns that it reads are not checked. The same declaration
is checked against each entry's merged frame before the feature runs. Validation and
execution therefore agree on the columns a feature reads. The sharpest refusal is a
multi-input join of mismatched entity granularity. Merging such inputs on the frame
alone pairs every row of one input with every row of the other in the same frame.
It can hold an op. Pipeline cannot express a tracker, a conversion or a
transcode; a recipe can, which is why a whole workflow from raw files or video to a
scored classifier is one document rather than a shell script wrapping a notebook.
The dataset records what was applied to it. On first use the recipe is copied to
<dataset>/.mosaic/pipelines/<digest>.json.
The digest is taken over the recipe's canonical form: keys sorted, defaults filled,
and a whole number in a step's params written as an integer, so 5 and 5.0
name one recipe.
One place per fact¶
Cross-step references sit at the exact site they substitute — inside inputs, inside
tracks, inside a params field. There is no edges array, so there is nothing that
can drift from the step bodies; the edge list is a derived view. The one explicit list
is after, and it corresponds to nothing in any payload because it is ordering only.
Planning resolves everything, and commits to nothing¶
plan_pipeline(ds, recipe) resolves every step in one topological walk and submits
nothing. Step A's identity is a function of its parameters, B's of its parameters plus
A's identity, C's of B's. The walk terminates because every term is either in the
recipe or already on disk: a feature-to-feature edge reads nothing, and a tracks
variant is minted from the recipe's settings rather than read back from tables no op
has written yet.
A resolved identifier is never load-bearing at execution. It drives the preview, the estimate, the validation and the decision to enqueue. It never skips a step and never enters a downstream job's payload — every step resolves its own identity at its own start. A submitted identifier is a prediction, not an instruction.
A graph that converts its own data¶
A convert-tracks or convert-labels step names the variant it writes from its
settings alone, so a step reading it through a tracks or labels reference is named
before the conversion runs. Which entries the conversion writes is not known until it
runs. On a dataset with no tracks yet, a step whose identity covers its entries, and
an op that is asked for entries, are left unresolved with the conversion as the
reason, rather than named over no entries. Each is named at its own start, when the
tracks exist. A conversion takes no scope, and on a dataset that already holds tracks
the plan covers those.
A step scope narrows one step¶
A request names the entries a graph covers. Request.step_scopes narrows single steps
within them, so one request can fit on the training groups and score on the test
groups. A step's target is the request's entries narrowed by its scope, and its
identity, its coverage and what it is asked for are measured over that target.
The data a step reads must be there for every entry of its target, so a step reading
another through inputs or tracks is refused when the other is narrowed to fewer of
them. A step reading another through a params reference reads one artifact, fitted on
whatever that step covered, and no such rule applies. A request that narrows no step is
written without the field, and planned and fingerprinted as before.
A fit is asked for all of its scope¶
A step whose result depends on which entries were in scope is asked for its whole
scope, never for the remainder. Its identity is its scope, so a fit over what is
left, recorded under the name of a fit over everything, is exactly the silent wrong
answer the scheme exists to prevent. A step with no scope dependence gets the
remainder, as it should. pair-posedistance-pca is the first when it fits and the
second when it is given a model, which applies a basis fitted by another step.
That asymmetry is also why a coverage shortfall is refused rather than proceeded
through, and refused only for a scope-dependent step. A scope-free step over 89 of 90
entries writes 89 correct outputs under the identifier they belong to, and the
ninetieth arrives later under the same one. A scope-dependent step over 89 writes
one artifact that is not the one anyone asked for, under a name saying it is.
allow_partial (--allow-partial at the CLI) is where a decision to proceed
regardless is recorded, and it answers
that question alone — a moved version or a disagreeing tracks variant is not a
question about how much, and no flag unlocks them.
A refusal names its reason¶
A refusal is a failure that names its reason, which tells it apart from a crash.
mosaic pipeline run, mosaic run and mosaic track exit with code 65 and print the
reason. Under --json they print one object with "status": "refused" and the
reason. From Python a refusal is raised as a Refusal
(mosaic.core.pipeline.refusal), with reason and a detail with the numbers
behind it. A pipeline step's refusal is its subclass StepRefused. The run log
records the attempt as failed, with the reason beside it.
The reasons form a closed set, such as coverage_shortfall, version_moved,
variant_mismatch and undecodable_codec. Each reason except coverage_shortfall is
fixed at its cause, and the step then runs again.
Failed entries: waiting and quarantined¶
A pipeline step that fails on one entry records the failure under
<dataset>/.mosaic/claims/, and the count carries across resubmissions. The count
decides what happens to that entry next:
- Waiting. After a failure the entry is not retried for a minute, and for two minutes after a second failure. A step that meets a waiting entry runs without it this time and records no decision about it.
- Quarantined. After three failures the entry is no longer attempted. A step whose
scope holds it is refused with
coverage_shortfall, naming the entry. Withallow_partialthe step proceeds without it, and that exclusion is recorded for that request only. For a fit this is a fit over fewer entries, named for the entries it covered.
A success clears an entry's record. To retry a quarantined entry once its input is
fixed, delete its record,
.mosaic/claims/entries/<name>/<run_id>/<group>__<sequence>.json (just
<sequence>.json when the group is empty), which also holds the entry's last error.
<name> is the step's directory under features/. Keep the rest of
.mosaic/claims/, since it is the only record of which entries are known to fail.
To build one, see Chain steps into a recipe.