DAG Guide¶
Overview¶
TidyRun provides three core building blocks for deferred computation:
Job: one deferred function call with named kwargsParametrizedJob: a parameter grid over keys that slices into nested jobsDAG: a key-addressable mapping of deferred nodes that can be evaluated to disk
A DAG evaluation writes outputs with the same storage contract used by
serialize(...), so deserialize(...) returns a matching LazyDict tree.
Core Concepts¶
Job¶
A Job holds a Python callable and named arguments.
from tidyrun import Job
def add(x: int, y: int) -> int:
return x + y
job = Job(func=add, kwargs={"x": 1, "y": 2})
Arguments can be plain values, LazyDict, Job, ParametrizedJob, or DAG.
ParametrizedJob¶
A ParametrizedJob represents a Cartesian-style parameterized computation.
Accessing a key fixes the first parameter:
- returns another
ParametrizedJobwhile parameters remain - returns a concrete
Jobwhen the last parameter is fixed
from tidyrun import ParametrizedJob
def score(model: str, split: str, prefix: str = "") -> str:
return f"{prefix}{model}:{split}"
grid = ParametrizedJob(
func=score,
parameter_names=["model", "split"],
parameter_values=[("m1", "train"), ("m1", "test"), ("m2", "train")],
kwargs={"prefix": "run="},
)
model_slice = grid["m1"]
leaf_job = model_slice["train"]
DAG¶
A DAG maps keys (same key type contract as keys.py) to nodes.
Supported node types are:
JobParametrizedJob- nested
DAG
from tidyrun import DAG, Job
def square(x: int) -> int:
return x * x
dag = DAG()
dag["a"] = Job(func=square, kwargs={"x": 3})
Evaluation¶
DAG.evaluate(...) is materialize-first:
- Compile the DAG into a plan directory (by default
<target>/plan) - Execute jobs in dependency order
- Serialize top-level outputs to the outputs directory (by default
<target>/outputs)
By default, jobs run in isolated subprocesses.
Core DAG Lifecycle APIs¶
These three methods cover the most common lifecycle for local and resumable execution:
- evaluate: one-call workflow that materializes a plan, executes it, and writes top-level outputs to your run outputs directory.
- materialize: compile only (no execution). Use this when you want a stable, inspectable on-disk plan before running jobs.
- execute_materialized: run an already materialized plan, optionally with skip_completed=True to resume partially completed runs.
Typical pattern:
- Use evaluate for everyday runs.
- Use materialize + execute_materialized for debugging, reproducibility, or resumable workflows.
Default layout for dag.evaluate("./exp1"):
- plan:
./exp1/plan - outputs:
./exp1/outputs
You can also skip target entirely when both paths are explicit:
result = dag.evaluate(
dag_path="./exp1-plan",
output_path="./exp1-outputs",
)
Sequential Evaluation¶
result = dag.evaluate("./sequential")
print(result["a"]) # 9
Execution Modes¶
Select execution behavior with execution_mode:
"subprocess"(default): isolated Python subprocess per job"thread": run jobs in the current process (lower overhead)"process": run jobs usingProcessPoolExecutor
As a rule of thumb, start with "subprocess" for the safest isolation and
reproducibility. Choose "thread" for lightweight local runs where low
overhead matters (for example during rapid iteration or tests). Choose
"process" when running many local CPU-bound jobs and you want process-level
parallelism with worker reuse through a process pool.
# Fast local testing (no subprocess spawn per job)
result = dag.evaluate("./thread-mode", execution_mode="thread")
# Process pool execution
result = dag.evaluate(
"./process-mode",
execution_mode="process",
max_workers=4,
)
Local Parallel Evaluation¶
Use max_workers to evaluate independent jobs in parallel.
# Thread pool (thread/subprocess modes)
result = dag.evaluate("./threaded", max_workers=4, execution_mode="thread")
# Process pool (process mode)
result = dag.evaluate("./process-pooled", max_workers=4, execution_mode="process")
Failure Handling and Resume¶
DAG execution fails fast. If any job fails, scheduling stops and a
DAGExecutionError is raised with structured context:
failed_job_idcausecompleted_jobscancelled_jobs
from tidyrun import DAGExecutionError
try:
dag.evaluate("./run")
except DAGExecutionError as exc:
print("failed:", exc.failed_job_id)
print("completed:", sorted(exc.completed_jobs))
print("cancelled:", sorted(exc.cancelled_jobs))
To resume after fixing a failing job, run from an existing materialized plan
and set skip_completed=True so already-written outputs are reused:
If outputs already exist and skip_completed=False (default),
execute_materialized(...) now raises an error to prevent accidental mixing
of previous and newly computed results.
plan_dir = dag.materialize("./run/plan")
result = dag.execute_materialized(
dag_path=plan_dir,
output_path="./run/outputs",
skip_completed=True,
)
Progress Logging¶
Use progress=True to emit simple progress logs during plan compilation and
job execution.
result = dag.evaluate(
"./run",
progress=True,
)
You can also provide a custom callback to collect or redirect progress lines:
messages: list[str] = []
result = dag.evaluate(
"./run",
progress=True,
progress_callback=messages.append,
)
If outputs are obsolete or wrong, clear them before resubmitting:
# Remove all outputs
dag.clear_outputs("./run/plan")
# Or remove specific job outputs only
dag.clear_outputs("./run/plan", job_ids=["train/model_a", "metrics/model_a"])
Custom Executor¶
You can pass your own concurrent.futures.Executor.
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=8) as pool:
result = dag.evaluate("./custom", executor=pool, execution_mode="thread")
Pass either executor or max_workers, not both.
Materialized Plan Helpers¶
For debugging/reproducibility, you can inspect materialized artifacts directly:
from tidyrun import load_callable, load_job_definition, load_job_inputs
plan_dir = dag.materialize("./experiment/plan")
definition = load_job_definition(plan_dir, "a")
func = load_callable(definition, plan_dir)
kwargs = load_job_inputs(definition, plan_dir)
print(func(**kwargs))
SLURM Executor¶
SlurmExecutor submits each job as an sbatch task, polling squeue for
completion. The plan directory and shared_dir must both be on shared storage
visible from all compute nodes. See the Executors guide for setup
instructions, resource configuration, and a full deployment example with git
commit pinning.
from tidyrun import SlurmExecutor
with SlurmExecutor(
shared_dir="/shared/tidyrun_scratch",
partition="compute",
time_limit="01:00:00",
memory="8G",
) as executor:
result = dag.execute_materialized(
dag_path="/shared/plans/run-001",
output_path="/shared/outputs/run-001",
executor=executor,
)
AWS Batch Executor¶
AwsBatchExecutor submits each job as a Batch container task, polling
describe_jobs for completion. The plan directory must be an S3 URI, and the
container image must call tidyrun-batch-entrypoint as its CMD. See the
Executors guide for the container setup, IAM
requirements, and a full deployment example with git commit pinning.
from tidyrun import AwsBatchExecutor
with AwsBatchExecutor(
job_queue="my-queue",
job_definition="my-worker:1",
) as executor:
result = dag.execute_materialized(
dag_path="s3://my-bucket/plans/run-001",
output_path="s3://my-bucket/outputs/run-001",
executor=executor,
)
End-to-End Example¶
from tidyrun import DAG, Job, ParametrizedJob
def metric(model: str, split: str, base: int = 1) -> str:
return f"{model}:{split}:{base}"
scores = ParametrizedJob(
func=metric,
parameter_names=["model", "split"],
parameter_values=[("m1", "train"), ("m1", "test"), ("m2", "train")],
kwargs={"base": 10},
)
summary = Job(func=lambda value: f"summary={value}", kwargs={"value": "ok"})
dag = DAG()
dag["scores"] = scores
dag["summary"] = summary
outputs = dag.evaluate("./experiment", max_workers=4)
print(outputs["scores"]["m1"]["train"]) # m1:train:10
print(outputs["summary"]) # summary=ok
Notes¶
- Evaluated outputs are serialized using the same metadata sidecar mechanism as the serialization API.
- The on-disk plan format is intended for reproducibility: you can re-run one
job later via
run_materialized_job(plan_dir, job_id). job_resourcesis keyed by top-level DAG keys and is only applied when the executor implementssubmit_with_options(...).
API Reference¶
tidyrun.job.Job
dataclass
¶
A deferred computation: a callable with named arguments.
Each argument value is an Operand, which may be a plain Python value, a LazyDict (existing on-disk outputs), another Job, or a DAG. Arguments are resolved recursively before the function is called.
Example::
def add(x: int, y: int) -> int:
return x + y
job = Job(func=add, kwargs={"x": 1, "y": 2})
Source code in src/tidyrun/job.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | |
rerun_snippet(*, dag_path, job_id)
¶
Return a Python snippet that reruns this job from a materialized plan.
Source code in src/tidyrun/job.py
101 102 103 104 105 | |
tidyrun.dag.ParametrizedJob
¶
Bases: DAG
A deferred computation indexed by parameter keys.
Parameters are declared through parameter_names and populated through
parameter_values. Accessing a key fixes the first parameter and returns
either a :class:Job (when one parameter remains) or another
:class:ParametrizedJob (when more parameters remain).
Being a subclass of :class:DAG, a ParametrizedJob inherits all
execution methods (materialize, execute_materialized, evaluate,
clear_outputs) with identical semantics: the top-level keys are the
first-level parameter values and no extra wrapping level is added.
Source code in src/tidyrun/dag.py
894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 | |
tidyrun.dag.DAG
¶
Bases: Mapping[Key, Node]
A mapping from keys to deferred computations.
DAG is the write-time dual of LazyDict: it maps :data:Key values to
nodes (:class:~tidyrun.Job, :class:~tidyrun.ParametrizedJob, or nested
:class:DAG instances). Evaluating a DAG to disk produces the same on-disk
layout as
:func:~tidyrun.serialize, so that :func:~tidyrun.deserialize returns a
:class:~tidyrun.LazyDict with the same key tree.
DAG execution is materialized-first: compile a plan on disk, then execute
each job in a dedicated Python subprocess with arguments loaded through
:func:~tidyrun.deserialize.
Example::
import tempfile, pathlib
from tidyrun.job import Job
from tidyrun.dag import DAG
def square(x: int) -> int:
return x * x
dag = DAG()
dag["a"] = Job(func=square, kwargs={"x": 3})
with tempfile.TemporaryDirectory() as tmp:
result = dag.evaluate(pathlib.Path(tmp) / "outputs")
assert result["a"] == 9
Source code in src/tidyrun/dag.py
495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 | |
materialize(dag_path, *, prefix=None, progress=False, progress_callback=None)
¶
Write job definitions and literal inputs for process execution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dag_path
|
Any
|
Destination for the materialized plan. May be a plain path
(definitions, inputs, and outputs are created as subdirectories)
or a :class: |
required |
prefix
|
str | None
|
Optional string prepended to all job IDs in this plan. |
None
|
progress
|
bool
|
When |
False
|
progress_callback
|
ProgressCallback | None
|
Optional callback that receives each progress message string. |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
The plan root directory ( |
Source code in src/tidyrun/dag.py
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 | |
execute_materialized(dag_path, executor=None, max_workers=None, job_resources=None, execution_mode='subprocess', skip_completed=False, skip_running=False, progress=False, progress_callback=None)
¶
Execute a previously materialized plan with dependency ordering.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dag_path
|
Any
|
Path to the materialized DAG directory. Outputs are always written
to |
required |
executor
|
Executor | None
|
Optional custom :class: |
None
|
max_workers
|
int | None
|
Number of workers for parallel execution. Creates a ThreadPoolExecutor if execution_mode is "thread", or ProcessPoolExecutor if execution_mode is "process". |
None
|
job_resources
|
Mapping[Key, Mapping[str, str | int]] | None
|
Optional per-node submission options. |
None
|
execution_mode
|
ExecutionMode
|
How to execute jobs: "subprocess" (default, isolated Python processes), "thread" (shared memory in threads), or "process" (ProcessPoolExecutor). |
'subprocess'
|
skip_completed
|
bool
|
When |
False
|
skip_running
|
bool
|
When |
False
|
progress
|
bool
|
When |
False
|
progress_callback
|
ProgressCallback | None
|
Optional callback used for progress messages. |
None
|
Source code in src/tidyrun/dag.py
607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 | |
evaluate(dag_path, executor=None, max_workers=None, job_resources=None, execution_mode='subprocess', skip_completed=False, progress=False, progress_callback=None)
¶
Evaluate this DAG to disk (materialize, then execute).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dag_path
|
Any
|
Directory for the materialized plan. Outputs are written to
|
required |
executor
|
Executor | None
|
Optional :class: |
None
|
max_workers
|
int | None
|
Number of local workers for parallel evaluation. When set,
creates a :class: |
None
|
job_resources
|
Mapping[Key, Mapping[str, str | int]] | None
|
Optional per-node submission options keyed by DAG key. This is
primarily useful with executors that expose
|
None
|
execution_mode
|
ExecutionMode
|
How to execute jobs:
|
'subprocess'
|
skip_completed
|
bool
|
When |
False
|
progress
|
bool
|
When |
False
|
progress_callback
|
ProgressCallback | None
|
Optional callback used for progress messages. |
None
|
Returns:
| Type | Description |
|---|---|
LazyDict
|
The deserialized :class: |
Source code in src/tidyrun/dag.py
741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 | |
clear_outputs(dag_path, job_ids=None)
¶
Delete serialized outputs for jobs in a materialized plan.
Use this to discard stale or incorrect outputs before resubmitting a
DAG. When job_ids is None the entire outputs directory is
removed. Otherwise only the specified jobs' output files are deleted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dag_path
|
Any
|
Path to the materialized DAG directory. |
required |
job_ids
|
list[str] | None
|
Optional list of job IDs whose outputs should be cleared.
When |
None
|
Source code in src/tidyrun/dag.py
838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 | |