| # Example config for `cascade-provisioner` β the per-round GPU pod renter. | |
| # | |
| # cascade-provisioner --config provision.toml --chain-toml chain.toml \ | |
| # --work-root /root/cascade/_train_work [--dry-run] [--once] | |
| # | |
| # This file is OPERATOR-LOCAL: keep it out of git (it names your SSH key and | |
| # hosts file) and OFF the public chain.toml. Round shape (epoch cadence, final | |
| # training hours, the image-digest pin) is read from chain.toml β this file | |
| # only holds the RENTAL policy: what to rent, where, and how much to spend. | |
| # | |
| # The provisioner holds NO wallet β only provider API keys (env: | |
| # SHADEFORM_API_KEY / LIUM_API_KEY, see deploy/cascade-provisioner.service) | |
| # and the orchestrator's SSH keypair. Signing/publishing stays on the trainer. | |
| [provisioner] | |
| # Where the trainer's --remote-hosts points. The provisioner writes this file | |
| # atomically per round (heat + final entries), re-renders it final-only when | |
| # the heat settles, and EMPTIES it when it can't rent β an empty file is the | |
| # trainer's explicit "train locally this round" signal. | |
| hosts_path = "/root/cascade/hosts.toml" | |
| # Crash-safe rental ledger (default: <work-root>/provisioner_state.json). | |
| # Restarts resume teardown from here; delete it only if you have verified by | |
| # hand that no cascade-* pod is still running on any provider. | |
| state_path = "/root/cascade/_train_work/provisioner_state.json" | |
| # Digest-pinned worker image (see docs/DEPLOY_PODS.md Β§1). MUST be @sha256 β | |
| # a moving tag breaks the expected_gpu / train_image_digest contract. | |
| image = "ghcr.io/tensorlink-ai/cascade-worker@sha256:0000000000000000000000000000000000000000000000000000000000000000" | |
| # ββ OR: bootstrap mode (no worker image published yet) βββββββββββββββββββββββ | |
| # With bootstrap_script set, pods rent BARE (image becomes the provider's | |
| # template to boot, digest pin not required) and the script β run ON the | |
| # orchestrator with POD_IP/POD_PORT/POD_USER/POD_KEY/POD_STAGE/POD_WORKDIR in | |
| # env β provisions them over SSH (rsync source + `uv sync --frozen`; see | |
| # deploy/pod_bootstrap.sh). The health gate then verifies what it built. | |
| # bootstrap_script = "/root/cascade/deploy/pod_bootstrap.sh" | |
| # bootstrap_timeout_s = 1800 | |
| # pod_user = "root" | |
| # Shadeform in bootstrap mode needs VM-mode launches: the account's registered | |
| # SSH key id (GET /v1/sshkeys) β pods boot as bare VMs, user "shadeform". | |
| # shadeform_ssh_key_id = "00000000-0000-0000-0000-000000000000" | |
| # Operator's static [[host]] entries (a long-lived hand-rented pod, e.g. the | |
| # final box) β validated at startup with the trainer's own loader, appended | |
| # verbatim to EVERY hosts.toml publish, and NEVER dropped by provisioner | |
| # activity (teardown re-renders, no-capacity rounds, budget refusals). | |
| # static_hosts = "/root/cascade/static_hosts.toml" | |
| # ββ elastic validator eval pod (optional; see [provisioner.eval] below) ββββββ | |
| # Where the VALIDATOR's --eval-hosts points β a SEPARATE file from hosts_path | |
| # (the two consumers have different lifecycles; the provisioner refuses equal | |
| # paths). Published when a round's manifest appears, cleared on teardown; the | |
| # validator re-reads it lazily at each offloaded eval, so clearing mid-round | |
| # is safe (its next eval just runs locally). Unset = eval stage off. | |
| # eval_hosts_path = "/root/cascade/eval_hosts.toml" | |
| # The eval pod's primary teardown signal: this deployment's validator | |
| # publishes round receipts at receipts/<validator_hotkey>/round-<id>.json β | |
| # when the served round's receipt appears the pod is torn down. A newer | |
| # manifest (round superseded) and the TTL below also end it. Unset = only | |
| # those two backstops apply. | |
| # receipt_prefix = "receipts/5ValidatorHotkey.../" | |
| # Orchestrator SSH keypair: the PUBLIC key is injected into each pod | |
| # ($SSH_PUBKEY); the PRIVATE key path is written into hosts.toml for the | |
| # trainer's dispatches (and used by the provisioner's own health checks). | |
| ssh_pubkey = "~/.ssh/cascade_ed25519.pub" # inline "ssh-β¦" also accepted | |
| ssh_key_path = "~/.ssh/cascade_ed25519" | |
| # Rent inside the last N blocks of the epoch (~12s each; 25 β 5 min). Must sit | |
| # within the trainer's reveal margin β the field is only countable once timed | |
| # reveals have landed β and be < epoch_blocks. Pods finish booting AFTER the | |
| # round starts; the trainer's --hosts-wait-seconds covers that gap. | |
| trigger_margin_blocks = 25 | |
| # HARD per-round spend ceiling in USD, projected at WORST case (every pod | |
| # billed for the full TTL). If the projection exceeds this, the round rents | |
| # NOTHING and the trainer trains locally β a runaway field can never surprise | |
| # you on the bill. | |
| max_spend_per_round = 25.0 | |
| # Pod time-to-live in epochs β the no-cooperation teardown backstop. Even if | |
| # the heat marker, the manifest, and the ledger are ALL lost, every pod dies | |
| # this many epochs after rent. | |
| ttl_epochs = 1 | |
| # Rules of escalation. A stage whose rung delivers NO healthy pod (the launch | |
| # call failed, or every pod and its one replacement was a dud) re-enters the | |
| # SKU ladder at the next (candidate Γ provider) rung β capacity re-probed at | |
| # escalation time, each rung re-checked against max_spend_per_round β for at | |
| # most escalate_deadline_s of wall clock per attempt. Renting runs in a | |
| # worker thread (the poll loop keeps tearing down / heartbeating); the | |
| # deadline bounds the single worker so the JIT final, the retries, and the | |
| # next round's trigger are never starved (0 = a failed rung is final). | |
| # A PARTIAL fleet below min_viable_fleet of its slot demand gets ONE | |
| # same-candidate top-up batch β never a different SKU (0 = never top up). | |
| escalate_deadline_s = 1800 | |
| min_viable_fleet = 0.5 | |
| # Within-round retry: a stage that rented NOTHING (no capacity anywhere, over | |
| # budget, or every pod a dud past escalation) re-attempts the full | |
| # pickβbudgetβrent pipeline on this cadence, for as long as the stage can | |
| # still matter β the heat while one serial screening wave fits in its | |
| # remaining window (the fleet re-sizes to that window), the final while its | |
| # full training hours + boot margin remain. 0 = one attempt per round. | |
| rent_retry_cooldown_s = 900 | |
| # When to rent the FINAL fleet. "margin": with the heat at the epoch boundary | |
| # (the default). "heat_complete": just-in-time at the trainer's | |
| # heat_complete.json marker β the fleet is sized off the marker's ACTUAL | |
| # finalist list and the pinned-SKU pods stop idle-billing through the heat. | |
| # Requires a trainer new enough to re-read hosts before the duel (it waits | |
| # --hosts-wait-seconds for final-tagged entries; boot is 10-15 min). If the | |
| # primary final rung probes scarce at the margin, the final rents early via | |
| # the ladder anyway β the thin-market exception. | |
| final_rent_on = "margin" | |
| # Health-gate knobs (see cascade/provision/health.py for the full seven). | |
| min_disk_gb = 20.0 | |
| # Pod-side layout (matches deploy/Dockerfile). | |
| remote_python = "/root/cascade/.venv/bin/python" | |
| workdir = "/root/cascade" | |
| # chain_toml = "/root/cascade/chain.testnet.toml" # only if non-default on the pod | |
| poll_seconds = 30 | |
| # ββ heat fleet: cheap SKU, sized off the revealed field ββββββββββββββββββββββ | |
| # Heat checkpoints are trainer-internal (screened and discarded, never | |
| # validated), so heats run on whatever is cheap β here 8ΓA6000 clusters. The | |
| # fleet is SLOT-sized: ceil(field Γ heat_hours Γ slot_overhead / window) GPU | |
| # slots, then ceil(slots / gpus_per_pod) pods, capped at max_pods. sku is the | |
| # EXACT `nvidia-smi --query-gpu=name` string β the health gate byte-compares | |
| # every GPU of every pod against it. | |
| # max_pods caps PODS ACROSS ALL SHAPES; pod count is always slot-derived, so a | |
| # high cap lets small shapes scale out without over-renting big ones. max_pods | |
| # = 0 marks the stage UNMANAGED (served entirely by static_hosts entries). | |
| # market_sku is the marketplace's alias when it differs from nvidia-smi's | |
| # device string ("A6000" on lium vs "NVIDIA RTX A6000" on the pod). | |
| [provisioner.heat] | |
| sku = "NVIDIA GeForce RTX 4090" | |
| market_sku = "RTX4090" | |
| gpus_per_pod = 4 # 4x cluster: one hosts.toml entry per GPU | |
| max_pods = 8 | |
| providers = ["lium", "shadeform"] # priority order; a stage never splits providers | |
| max_price_hr = 2.60 # USD per POD-hour; dearer offers are skipped | |
| slot_overhead = 1.3 # pads for image pull / corpus build / one flaky retry | |
| # Ordered SKU/shape fallbacks: the round takes the FIRST candidate Γ provider | |
| # with capacity for the whole heat fleet (pod count re-derived per shape). A | |
| # stage never mixes candidates, so within-round fairness holds by construction; | |
| # heat scores only rank within a round, so cross-round SKU variance is free. | |
| [[provisioner.heat.candidate]] | |
| sku = "NVIDIA RTX A6000" | |
| market_sku = "A6000" | |
| gpus_per_pod = 4 | |
| max_price_hr = 2.40 | |
| [[provisioner.heat.candidate]] # bigger cluster if that's what's stocked | |
| sku = "NVIDIA GeForce RTX 4090" | |
| market_sku = "RTX4090" | |
| gpus_per_pod = 8 | |
| max_price_hr = 5.00 | |
| [[provisioner.heat.candidate]] # last resort: 2x is the floor β 1x | |
| sku = "NVIDIA GeForce RTX 4090" # singles pay a full bootstrap per | |
| market_sku = "RTX4090" # lane and rarely beat the trainer's | |
| gpus_per_pod = 2 # local fallback | |
| max_price_hr = 1.30 | |
| # ββ final fleet: the pinned SKU, king + finalists on ONE pod βββββββββββββββββ | |
| # Default shape is a single pod with gpus_per_pod >= 1 + [round] finalists: | |
| # every final run reports the same physical box's GPU, so the validator's | |
| # expected_gpu pairing holds by construction. One 2ΓL40S pod = king + 1 | |
| # finalist. (gpus_per_pod = 1 falls back to one pod per run.) | |
| [provisioner.final] | |
| sku = "NVIDIA L40S" | |
| market_sku = "L40S" | |
| gpus_per_pod = 2 | |
| max_pods = 2 # 0 = unmanaged: a static hand-rented pod serves finals | |
| providers = ["lium", "shadeform"] | |
| max_price_hr = 3.0 | |
| # ββ eval pod: the validator's per-round offload box (OPTIONAL) βββββββββββββββ | |
| # Manifest-triggered, unlike heat/final: rented when a round's manifest | |
| # publishes (that is when the validator's GIFT-Eval gate + cascade bench need | |
| # GPU β exactly as the trainer fleet is torn down), torn down when the round's | |
| # receipt appears under receipt_prefix, a newer manifest supersedes it, or the | |
| # TTL fires. One pod (max_pods = 1), one GPU is plenty. Absent table (or | |
| # max_pods = 0, or eval_hosts_path unset) = feature off: validators eval | |
| # locally on their --device, the pre-elastic behaviour. [[provisioner.eval. | |
| # candidate]] SKU fallbacks work exactly like the heat's. | |
| # [provisioner.eval] | |
| # sku = "NVIDIA L40S" | |
| # market_sku = "L40S" | |
| # gpus_per_pod = 1 | |
| # max_pods = 1 | |
| # providers = ["lium", "shadeform"] | |
| # max_price_hr = 1.20 | |
| # ββ per-provider pod paths (bootstrap mode) ββββββββββββββββββββββββββββββββββ | |
| # Providers boot different base users: lium pods are root, shadeform VMs land | |
| # as the "shadeform" user. Absent providers use the global defaults above. | |
| # [provisioner.pods.shadeform] | |
| # user = "shadeform" | |
| # workdir = "/home/shadeform/cascade" | |
| # remote_python = "/home/shadeform/cascade/.venv/bin/python" | |