Launch Plan & Preflight¶
launch_plan
¶
Resolve a config into a launch plan, and stage it — everything before anything launches.
The offline half of a run. plan_run answers "what would this config schedule, and has it run
already" without touching a thing; stage_run goes one step further and uploads the artifacts a
remote launch needs (the config, and for Spark the code zip), then writes a reproducibility
manifest — and still submits nothing. Neither ever starts a job; that is main.run and
job_launch.
Two things make the plan trustworthy rather than merely informative:
- Same id as the real run.
run_idis a pure digest of the whole config, so a plan resolves the id a live run would use. That is what lets the CLI's--dry-runreport an existence verdict (Idempotency) — this exact config has already run, and here is its status — and what makesstage_run's uploads land at the paths the launch will read. - The source is pinned, but not into the digest.
lock_profile_sourcerewritesprofile.source: "auto"to the concrete run it resolves to, so a staged config reproduces its original fleet instead of re-searching.registry.idsthen excludes that field from the digest: it is resolved, not authored, and a config whose identity depended on what had been run before it never converged. Two runs a week apart share a run_id and dedupe on read; their manifests and sizing provenance record the different evidence each was sized from.
LaunchPlan is the one return type for both, distinguished by staged: plan_run returns URI
templates (where artifacts would go) and stage_run returns the real ones (where they now are),
along with commands that are actually runnable. The command strings are built by commands from
the resolved deployment envelope, so what is printed is what a launch would execute — the two-tier
emit (a portable python -m line, plus the platform-native gcloud form).
Both take an optional Settings; without a reachable SF_* environment plan_run degrades
rather than fails (a plan with an unknown verdict and no commands), because resolving a config is
useful offline. stage_run requires it — staging touches GCS.
feasibility_report is the deliberate exception to "the plan is offline": it reads one
aggregation off the source panel, because how many backtest folds each series actually achieves is
a fact about the data and no amount of config inspection will produce it. It is opt-in
(--feasibility), reads only, and degrades to a line of text when there is no environment.
Public surface: plan_run, stage_run, lock_profile_source, feasibility_report, and
the LaunchPlan / Idempotency result types.
Idempotency
dataclass
¶
Whether a run's config has already been submitted (the pre-submit existence check).
run_id is a pure digest of the config, so an unchanged config re-derives the same id — this
verdict says whether that id already has a header in the registry. checked is False when
the registry couldn't be consulted (no SF_* env, or no reachable registry): the verdict is
simply unknown, not "new". prior_status is the existing run's status when exists (e.g.
COMPLETED), else None.
LaunchPlan
dataclass
¶
LaunchPlan(
run_id,
python_runtime,
python_models,
bq_models,
fanout,
staged,
config_uri,
commands,
idempotency,
force,
nodes,
variant_plan=(),
)
A run resolved to launch-ready form: id, runtime split, fanout, and the launch commands.
Produced offline by plan_run (staged=False — URIs are the templates where artifacts will
land; no GCS is touched) or by stage_run (staged=True — artifacts uploaded, URIs real and
the commands runnable). commands maps a tier name to its LaunchCommands: "main",
"ray", and one "spark:<family>" per Serverless family ("spark:statistical", …) —
per-family because each family resolves its own hardware and batch id, so one command cannot
stand for all of them. It is None only when the GCP infra identity can't be resolved offline
(no SF_* env), in which case the run_id + fanout + runtime split are still returned.
idempotency is the exists-vs-new verdict for this config's id; force records that the
caller intends to re-run an already-run config (it only shapes the emitted guidance — a re-run
is idempotent regardless, via dedupe-on-read under the shared run_id).
nodes is the run's execution DAG resolved offline (dag.dag_nodes): one node per family job
plus the ensemble, each with its deterministic job_key and upstream dependencies — the
plan's cross-system identity map, before anything runs.
feasibility_lines
¶
The feasibility report for a measured panel, as lines to print (pure).
What plan --feasibility prints once it has the counts. Three things, in the order an
operator needs them: how much work this is (cells, fits, and the honest backtest multiplier),
who actually gets scored (the achieved-fold cohorts, which on a ragged panel are not one
number), and what to change if the answer is unwelcome (backtest.suggest_min_train).
Advisory only — it reads nothing but the counts and writes nothing at all. min_train is a
config field, so acting on the suggestion moves the run_id, which is the caller's decision to
make deliberately rather than something a planning verb should do for them.
Source code in src/scale_forecasting/launch_plan.py
feasibility_report
¶
Read the panel's series lengths and report what the run's fold geometry does to it.
The --feasibility half of the plan verb: the one part of planning that cannot be honest
offline, because how many folds a series achieves is a fact about the data, not the config.
Best-effort like every other reporting verb — an unreachable environment produces a line saying
so rather than an exception, so --dry-run --feasibility still returns a plan with no
SF_* env.
Source code in src/scale_forecasting/launch_plan.py
lock_profile_source
¶
Replace compute.profile.source = "auto" with the concrete reference it resolves to.
The lockfile step. Pinning is what makes auto convenient and reproducible: re-running a
staged config reproduces the original fleet exactly, because what actually resolved is written
into it rather than re-searched.
It deliberately does not move the run_id. registry.ids excludes this field from the
digest, because it is resolved rather than authored — see _canonical_config, which explains
what went wrong when it was included. Two auto runs a week apart therefore share an id and
dedupe on read, while their staged manifests and sizing provenance record the different
evidence each was sized from. That is the distinction: a run's identity is what was asked
for; its provenance is what answered.
What gets pinned is the pointer, never the numbers. The rows behind a run_id are
immutable, so the pointer is as good as the values and keeps the config small enough to stay
readable. When discovery finds nothing, the pin is "baseline" rather than "auto": the
remainder of the chain (shipped baseline, then static) is deterministic, so pinning it makes
the run reproducible instead of leaving it to re-search later.
Degrades to the unlocked config when the registry cannot be reached — a plan produced with no
SF_* environment is a preview, and it says so elsewhere too (commands=None, verdict
unknown). The run still resolves its source at sizing time; it just is not pinned.
Source code in src/scale_forecasting/launch_plan.py
plan_run
¶
Resolve a run offline to its id, fanout, runtime split, and launch-command templates.
The "plan" verb: pure and GCP-free. Computes _plan and estimate_fanout, then — best-effort —
resolves the infra identity, consults the registry for the exists-vs-new verdict
(_check_idempotency), and
builds the two-tier launch commands with the URIs the artifacts will land at (nothing is
uploaded). When the infra can't be resolved offline (no SF_* env), the commands are omitted
(commands=None) and the verdict is unknown, but the id/fanout/split are still returned, so
a plain --dry-run works with no environment. force shapes only the emitted re-run
guidance. Emits the plan to the log and returns it.
Source code in src/scale_forecasting/launch_plan.py
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 | |
preflight_short_series
¶
Enforce backtest.short_series="error" at submit time, where the operator is watching.
main.run calls this once, before it launches anything. It is the useful place to refuse a
run under that policy: the message reaches the terminal the operator typed into, no cluster has
been provisioned, and no registry rows exist to clean up. Each Python engine's driver holds a
second check (spark_io.assert_source_supports_folds and the Ray twin) for the paths that do
not come through here — a staged config launched from an emitted command, a Composer task, or
an engine driven directly.
Costs one aggregation, and only when the policy is armed; every other value of
short_series returns before touching BigQuery, because those policies have already decided
what a short series gets.
A failed read warns rather than refuses. short_series="error" means "stop if the panel
is too short", not "stop if I could not tell" — turning a transient BigQuery hiccup into a
refused run would be a worse failure than the one this guards against. The driver backstop
still runs, on a panel the engine has by then definitely read, so nothing is skipped by
letting this one go.
Source code in src/scale_forecasting/launch_plan.py
read_series_lengths
¶
Observation count of every series the run will forecast — one BigQuery aggregation.
SELECT ts_id, COUNT(*) … GROUP BY ts_id, honouring data.series_limit the same way the
engines do. One scan of two columns with a single shuffle: cheap enough to run before a launch,
which is the whole point of feasibility_report.
The counts are raw rows, and raw rows are the exact input a fit sees — features.build_features
creates lag columns but does no dropna, so nothing shrinks the panel between here and the
model. No lag-warmup correction is needed and applying one would understate every series.
Source code in src/scale_forecasting/launch_plan.py
resolve_settings
¶
Resolve Settings from the SF_* env (raises ConfigError when unset).
stage_run
¶
Stage a run's artifacts to GCS and return the runnable launch commands — no submit.
The "stage" verb: uploads the config (and, for Spark, the code zip + launcher shim) to the code
bucket, builds the launch commands against those real URIs (so they run as-is from any ADC
box), and writes the reproducibility manifest runs/<run_id>.plan.json next to the config.
Unlike plan_run, the infra identity is required — staging touches GCS — so a missing SF_*
env raises rather than degrading; the exists-vs-new verdict (_check_idempotency) is therefore
always resolved. force shapes only the emitted re-run guidance. Returns the LaunchPlan
with staged=True.
Source code in src/scale_forecasting/launch_plan.py
1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 | |