Offloading to a cluster

A recipe that is purely declarative can be compiled to a cluster workflow and handed off, so the pipeline runs without a live ninja process babysitting it. This is what ninja compile does.

When a recipe can be offloaded

Offloading requires that the whole recipe be statically knowable – the compiler must be able to determine every job and every dependency without running any Python. A recipe is offload-eligible only when:

  • it has no orchestration functions (nothing whose behaviour depends on live Python control flow),

  • it has no MUTABLE inputs, and

  • only paths cross between steps (an output wired into a later input must be a filesystem path knowable at compile time, not a wrangler-derived value).

Anything relying on live Python is rejected with an explanation – run those recipes locally with ninja run instead.

A minimal offloadable recipe

This mirrors examples/offload_demo.py: two steps wired by a single filesystem path – make touches a file, use reads it.

from pathlib import Path

from pydantic import BaseModel

from shinobi.steps import Cab, InputRef, OutputRef, ParamMeta, Recipe, StepRef


class PipeInputs(BaseModel):
    target: Path = Path("made.ms")


class TouchInputs(BaseModel):
    out: Path


class PathOutputs(BaseModel):
    out: Path | None = None


class CatInputs(BaseModel):
    f: Path | None = None


class OkOutputs(BaseModel):
    ok: bool = True


make = Cab(name="make", command="/bin/touch", inputs_model=TouchInputs,
           outputs_model=PathOutputs, field_meta={"out": ParamMeta(positional=True)})
use = Cab(name="use", command="/bin/cat", inputs_model=CatInputs,
          outputs_model=OkOutputs, field_meta={"f": ParamMeta(positional=True)})

pipe = Recipe(
    name="pipe",
    inputs_model=PipeInputs,
    outputs_model=OkOutputs,
    steps=[
        StepRef(name="make", step=make, wiring={"out": InputRef(field="target")}),
        StepRef(name="use", step=use, wiring={"f": OutputRef(step="make", field="out")}),
    ],
    output_wiring={"ok": OutputRef(step="use", field="ok")},
)

Because the only thing crossing between steps is a path (make’s out output is a passthrough of its out input, so it is known statically), the recipe is offload-eligible.

Compile it

Preview the compiled Slurm workflow without submitting anything – no cluster needed:

$ ninja compile myrecipe.py:pipe --target /scratch/made.ms --container-runtime none

This prints two sbatch scripts linked by --dependency=afterok: make first, then use once make succeeds.

Or run the same recipe locally instead, driven in-process:

$ ninja run myrecipe.py:pipe --target /tmp/made.ms

Submit and detach

Add --submit to hand the workflow to a real Slurm cluster and detach. A handle file is written under <workdir>/.shinobi/<recipe>/handle.json:

$ ninja compile myrecipe.py:pipe --target /scratch/made.ms \
    --container-runtime none --submit

Check on it later

ninja status queries the engine fresh from the handle file – there is no persistent process to keep alive:

$ ninja status /scratch/.shinobi/pipe/handle.json

Note

The Slurm engine was not live-verified against a real cluster in the development environment. See AGENTS.md in the repository for what that means in practice.