You submit a data generator — a purely algorithmic process behind the
generate() endpoint: a sampler built from priors (GP/kernel families, causal
DAGs, parametric trend/seasonality/noise, …). It is code-only — no shipped
weights (see the contract below), so you compete on the generator's synthetic data,
not on a large pretrained forecaster distilled into a "generator". Whatever it is,
it produces synthetic time series for the fixed forecaster in the effective
era contract. Pre-v2 eras use the Toto2 lineage; v2 A/B use a fresh Cascade
MoE lineage, with a configured random-init window and checked warm starts.
Lane A challenges the king; Lane B trains selected experts and must satisfy
its target/acceptance rules. Lane C's MarketCast recipe and pinned Timestral
base have a separate workflow.
This document defines the generator surface, not a single permanent model size.
Read in order: layout → Python interface → required checks → local verification. The channel and record sections explain optional ways to structure your output.
Use the active era's signed training contract and the running trainer's
generation policy. These checked-in mainnet values are a reference, not proof
of deployment. Local verify uses [generator] limits; the CPU shard tier
uses [generation] max_length, which can allow longer series. Longer output
does not increase the model context: v2 crops training windows from it.
| Setting | Mainnet before v2 | Mainnet v2 A |
|---|---|---|
| Model preset | toto2-4m |
cascade-moe-v2-120m |
| Corpus feed | stream_cpu |
shards |
| Budget accounting | series_points |
points |
| Batch accounting | series |
sequences |
| Batch size | 64 |
512 |
| Model context length | 4096 |
4096 |
| Training wall limit (seconds) | 18000 |
18000 |
| Generator channels per yield | 1–32 | 32 |
| Generator length limit | 64–4096 | 64–32768 in the CPU shard tier |
| CPU shard generation window | Not used by this contract | 90 minutes |
Testnet has different activation and budgets. Before a live comparison,
refresh cascade status --json; use the same network, contract, base and
evaluation settings on both sides.
cascade verify DIR --live --json resolves the deployed contract and CPU-tier
policy together. Under v2 shards it uses [generation] max_length and
child_memory_mb, including 8,064-point series within those bounds. With no
bound contract (no --live, no --at-block), verify, deploy, an
unfunded submit, iterate, score and fetch --verify check the
intersection of the base and [v2.training] policies: exactly 32 channels and the
stricter length, memory and time limits, so a pass is admissible whichever
side of the launch trains the entry. Use --live for the exact deployed
policy, including longer v2 shard series. --at-block selects a configured
era explicitly; while the mainnet launch is dated and [v2] from_block is
still 0 it cannot resolve that launch offline and exits with usage error
launch_unresolved rather than silently binding the pre-v2 policy. Pre-v2
verification retains the one-channel floor and its earlier length limit. Do
not use --skip-verify to bypass a policy mismatch.
Quick and full runtime checks execute generator code in bounded sandbox children. Shard-aware verification also pins each child to the tier's configured cores per child and caps BLAS threads before importing generator dependencies, within the local host's allowed cpuset. Legacy verification retains its earlier CPU policy. Full verification compares two finite corpus prefixes; it cannot prove that every future seed or the complete production budget will succeed. A host unable to provide the configured isolation must use an approved container/runtime or report the check as unavailable.
V2 synthetic generators must yield (C, L) arrays with C = 32 on
every yield ([generator] min_channels = 32, max_channels = 32).
One-dimensional arrays and arrays with 1–31 or more than 32 channels are rejected.
This also applies to a record's values array. The private evaluation pool's
channel range is independent. Historical v1 replay retains its earlier limits.
Channel economics:
- Billing.
[training] budget_denominationdecides what one budget point is. The rule in force for an era is thebudget_denominationin the signedcontract_bodyof that era's manifests (manifests/latest.jsonon the manifest bucket); a scheduled switch (budget_denomination_after/budget_denomination_after_block) applies to eras starting at or after its block, and a leg always trains, settles and is audited under its era's rule."points+mv<N>"(an optional policy, illustrated withpoints+mv20): every channel value is a point, and a series withC > 1is billedceil(C × L × 100 / (100+N)). An all-multichannel corpus trainsN %more tokens than a univariate one; a corpus that stacks only a share of its series earns proportionally less; a copied or junk channel spends real budget. The contract's wall (max_train_seconds) still applies: a leg that cannot finish its budget in time stops there (deadline_hit)."series_points": a(C, L)series costsL, so a C-channel corpus trainsC×the channel tokens of a univariate one from the same budget — but wider batches cost more GPU time, so a width the GPU cannot push through in its wall (max_train_seconds) stops at the wall under budget (deadline_hit). Size C to the GPU, not to the budget.
- Batch size and budget are separate. With
batch_denomination = "series"andbatch_size = 64, a C-channel batch contains 64 series, or 64 × C channel sequences. Underseries_pointsthis preserves the budgeted update count across widths; other denominations can change it. The wall can still stop a slow run early. Mainnet v2 usesbatch_denomination = "sequences"withbatch_size = 512: each step holds 512 ÷ 32 = 16 series of 32 channels (max(1, batch_size // C)initer_training_batches). - Cross-channel structure only pays if it is real. Multivariate eval
windows are scored jointly (all channels forecast in one pass; the window
counts once), and eval windows carry at most 32 channels whatever your C.
Near-duplicate channels are flagged by the channel-redundancy telemetry
(
[generator] channel_corr_mode = "shadow"), and so are unrelated rows stacked into one array: a channel whose innovations are uncorrelated with every other channel is logged as unpartnered (channel_telemetry.frac_unpartnered). Both are labels only today. - Univariate and 2–7-channel yields are historical examples only; they are not valid new v2 submissions. Each channel should add useful signal.
A worked eight-channel example with billing numbers: MINER.md §11.
Each generate() yield may be a named-field record instead of a bare array:
yield {"values": arr} # identical to `yield arr` — same corpus digestRecords accept only fields named by the effective contract's
accepted_fields; unknown names are always rejected. Cascade MoE Base's new
pyramid contracts accept groups. Generator masks and covariate roles are
not accepted by this recipe. The legacy Toto2 backend has separate consumers
for mask and roles; that does not make them valid for the pyramid recipe.
start, freq, group_id, labels and quantiles stay reserved and refused.
# 32 channels, four groups of eight; interleaving is also allowed.
yield {"values": values.astype(np.float32),
"groups": np.repeat(np.arange(4, dtype=np.uint8), 8)}groups has shape (C,), one integer label per channel. C is 32 in the checked-in v2 policy,
while an individual group can have 1–C channels. Labels introduce new
numbers as 0, 1, … in order of first appearance; gaps, reordered first
appearances, negatives and fractional labels are rejected. Each group's
channels attend only to each other; groups do not connect separate yields.
They share the same length but can describe independent processes. Time crops
leave labels unchanged. Batching uses equal-width buckets, with no channel
padding. Every value still counts toward the normal point and batch budgets.
Miners choose the packing; no group-size distribution is required. For
example, 12 related channels + 5 + 3 + 12 independent singletons fills all
32 slots, with labels [0]*12 + [1]*5 + [2]*3 + list(range(3, 15)).
The checked-in mainnet v2 batch uses batch_size=512 with
batch_denomination="sequences": 16 examples × 32 channels per optimizer
batch. Its eight micro-batches split that into two examples at a time.
This matches channel-series count; it is not a measured GPU-cost guarantee.
Draw group sizes and labels deterministically from the generator seed. Your
own config.json controls the size distribution. The starter's optional
packing.enabled defaults to false; enable it only under a contract that
accepts groups. Its example size-bin weights for 1 / 2–4 / 5–8 / 9–16 /
17–32 channels are 30 / 25 / 25 / 15 / 5. These are draw weights, not
mandatory corpus proportions: the final group is clipped to the remaining
channels. Each group gets an independently drawn driver. Full verification
checks label determinism along with values.
Omitting groups or setting it to None preserves values-only output, losses,
digests and legacy schema-1 shard bytes. Explicit schema-3 storage uses the
same lossless float32 values and group labels with different Parquet bytes.
An explicit all-zero vector has the same
attention/sampling behavior but contributes to the record digest. Other group
vectors use schema-2 shards and their labels are covered by shard and stream
hashes. The copula shares one latent within each group, independently of the
other groups. It does not change the loss definition.
Interface version 2 announces group packing. Under an accepting contract
you may declare "interface_version": 2 in config.json. Older declarations
and bare arrays still work; declaring a version newer than the effective
trainer supports fails early. Check fresh live policy: this feature requires
the matching new architecture, images and checkpoint lineage before live use.
The per-series group_id remains distinct and unsupported
(decision).
In a stream_cpu feed ([training] corpus_mode) the trainer consumes
your series in yield order (bucketed by shape, no global shuffle).
Ordering easy→hard, stationary→shifted, or annealing your mixture over the
stream is a curriculum you already control — it is the honest answer to "how
do I express non-stationarity to this trainer": order, not timestamps
(DEC-CA-0025). Both duel sides hold the same lever, and a pathological order
can reduce throughput. The shard tier combines independently seeded children;
do not assume its global order matches one streaming generator call.
Your generator repo (a local directory deploy pushes to the Hippius Hub registry)
must contain at least:
generator.py # exposes `class Generator(DataGenerator)`
config.json # any JSON object; your generator may read it
requirements.txt # hash-locked, allowlisted, <= max_packages
No shipped data — generators are code-only. Weight files of any kind are
rejected: pickle checkpoints (*.bin, *.pt, *.pth, *.ckpt, *.pkl, …)
because loading them runs arbitrary code, and code-free containers
(*.safetensors, *.npy, *.npz, *.onnx, …) because they'd let you distill a
pretrained model into the generator. The same policy covers RAW DATA: bulk
numeric payloads embedded in config.json or as python literals — real series
included — are outside the design space, not a loophole (DEC-CA-0028; the repo
byte cap is the wall). torch/gpytorch stay available as compute libraries
for GP/kernel priors, and fitting parameters inside the sandbox at run time
from your own procedural data is legal — just don't ship parameters or data.
The whole repo must be <= max_repo_mb (small, since it's source + config).
A future config arming ([training] real_corpus_ref, DEC-CA-0028) may give
every generator read access to ONE owner-published, digest-pinned real-data
corpus — identical bytes on every machine. Opt in by declaring the keyword:
def __init__(self, config_dir: str, *, seed: int, real_corpus_dir: str | None = None):
...While unarmed (today) the kwarg is never passed, so it MUST default. When
armed, real_corpus_dir is a read-only local directory; derive your output
from the corpus contents only and treat the path itself as opaque — it
differs per machine, so baking it into your output makes your corpus
non-reproducible across hosts and fails the audit (entry lost). Constructors
without the keyword keep working under every config; they simply don't see
the corpus.
from collections.abc import Iterator
import numpy as np
from cascade.interface import DataGenerator
class Generator(DataGenerator):
def __init__(self, config_dir: str, *, seed: int) -> None:
# Load config_dir/config.json if you like. `seed` is your ONLY source
# of randomness — derive everything from np.random.default_rng(seed).
...
def generate(self, n_series: int) -> Iterator[np.ndarray]:
# Yield UP TO n_series float arrays (C, L), with C = 32.
# Every yield must satisfy the active channel range. Each length L must fall in the configured
# [min_length, max_length] band; total emitted points (C*L) are capped.
# The trainer may stop consuming early — at the training token budget
# (streaming) or at [generator] corpus_target_points (materialised
# drain) — so the consumed PREFIX must be deterministic in (seed, n).
...
@property
def name(self) -> str:
return "my-generator"- Determinism. Two runs at the same
seedmust produce a byte-identical corpus. No wall-clock, noos.urandom, no un-seeded global RNG. If your generator uses torch, seed it too (torch.manual_seed(seed)+torch.use_deterministic_algorithms(True), on CPU).cascade verifyruns your generator twice and rejects it if the digests differ — non-negotiable, because the trainer and validators rely on it to audit runs. - Bounds. Each series is finite (no NaN/inf), floating dtype, shape
(C, L)withgenerator.min_channels <= C <= generator.max_channels(32 for v2) andLin the active length band described above. Local/full materialisation is capped bygenerator.max_total_points; the shard tier has its own point target and CPU window. Keep magnitudes in a sane float32 range: a corpus that drives the loss non-finite is rejected as the miner's fault. - Count / budget. For ordinary materialised verification, with
[generator] corpus_target_pointsarmed (it ships armed), the materialised drain (cascade verify,cache_reuse) stops once your corpus reaches the target points, and the series count is yours to choose — many short series or fewer long ones (DEC-CA-0031; compute-heavy GP/kernel priors want short — see "Compute-heavy priors" below). The streaming feed likewise stops at the training token budget.generate(n)may therefore be stopped beforen; if the owner disarms the target (corpus_target_points = 0) the legacy rule returns: exactlynseries are required. Yielding more thannis always an error, and exhausting your iterator below the points target rejects the run. - No network / no escape. Every
.pyin your tree — and any Python source packed into a string constant — is AST-scanned for blocked imports (sockets, subprocess, pickle, multiprocessing, the cascade internals, …), compiled/native modules (.so,.pyd,.pyc, …) are rejected, and the generator runs in a network-isolated sandbox. Ship every module as a.pyfile: under[static_guard] packed_sources = "reject"the trainer refuses Python code inside a string constant at admission, and the rejection spends the hotkey (MINER.md §11). Seechain.toml [static_guard]. - Dependencies & size.
requirements.txtlines must bepkg==ver --hash=sha256:…, drawn fromchain.toml [dependencies] allowed(which includestorch/gpytorchas compute libraries for GP/kernel priors — but no shipped weights), at mostmax_packages. The fetched repo (code only) must be<= max_repo_mb.
The submitted entry point is Python. Native/compiled modules are rejected, and spawning a Rust/C++ executable is not a supported generator path. NumPy and Numba are allowlisted compute libraries; their presence does not waive sandbox, determinism or budget checks. Supporting another source language would require a versioned build/runner contract and equivalent isolation and output checks.
gpytorch, scikit-learn and networkx are allowlisted so GP, kernel and
graph priors can compete, but a naive implementation prices itself out. Three
facts to design around (DEC-CA-0031). The CPU-second limits below describe
the single sandbox path. The v2 CPU tier additionally runs seed-sharded child
processes under [generation] parallelism and its own window_seconds wall
(default 5,400 seconds). A 90-minute tier window is not a promise that an
arbitrary corpus finishes; generation, serialization/hashing and upload need
measurement. Use the effective generation profile's length/point limits.
For the single sandbox path:
-
The sandbox CPU budget counts threads. The sandbox enforces
max_generate_secondsasRLIMIT_CPUacross every thread, andmultiprocessingis blocked. Multi-core BLAS burns the budget faster. Pin linear algebra to one thread (OMP_NUM_THREADS=1is set on lane-pinned pods) and spend the saving on a cheaper factorisation. -
The corpus budget is points, not series. Dense GP draws using a fresh covariance Cholesky factor scale roughly cubically in length: at L=1024 instead of 4096 a kernel prior costs ~64× less per series. Emit many shorter series; the length band is the only shape constraint.
-
Factorise with relative jitter, not SVD fallbacks. The "Cholesky, else SVD" pattern fires constantly on near-singular kernels and the fallback is ~10× slower. Scale the jitter to the kernel's own magnitude and escalate:
def stable_cholesky(K, max_tries=6): base = np.mean(np.diag(K)) for i in range(max_tries): try: return np.linalg.cholesky(K + (1e-9 * 10**i) * base * np.eye(len(K))) except np.linalg.LinAlgError: continue raise np.linalg.LinAlgError("kernel not factorisable at max jitter")
Measured on our GP priors: ~5× per draw, SVD fallbacks from 47 per corpus to 0. Deterministic, so your digest stays reproducible.
Generator speed is a compute multiplier in the live stream (DEC-CA-0001): a
slow generator feeds the trainer less data inside the leg's wall. Measure
seconds/series × (points needed / points per series) against the budget
before you submit; cascade score is the ground truth.
cascade verify ./my-generator-repo # local source, output and determinism checks
cascade deploy ./my-generator-repo --hub-repo <namespace/name> \
--wallet-name <coldkey> --wallet-hotkey <hotkey>deploy verifies the repo locally, pushes it to your Hippius Hub repo (OCI),
and writes metro-v1:gen:hippius:<repo>@<digest> on-chain as a timelock
commitment (sealed in the SCALE-prefixed encoding validators decode — never
hand-roll it with bittensor.timelock.encrypt). The OCI digest content-addresses (and so pins) the exact
tree the trainer will fetch — needs the [hippius] extra and Hub credentials
(HIPPIUS_HUB_TOKEN, or HIPPIUS_HUB_USERNAME + HIPPIUS_HUB_PASSWORD). Already
pushed? Pass --ref <repo@digest> to skip the upload and just commit.
The timelock reveal defaults to a timed reveal: the payload decrypts
[round] reveal_margin_blocks before the next epoch boundary, so a submission
stays hidden until then and cannot be copied ahead of you
(--reveal-now / --blocks-until-reveal N / --next-epoch override). Prefer
--hub-namespace <ns> over a fixed --hub-repo name — each deploy then uses a
fresh non-guessable repo, keeping the content as undiscoverable as the pointer.
See MINER.md §5 "Reveal timing". Private
submissions (cascade submit) skip the Hub entirely: the ZIP goes to the
operator's vault and the chain commit is a vault/direct@sha256:… ref — see
MINER.md §5.
You're optimising for downstream forecast generalisation of the effective contract's model trained on your data from its pinned init/base, evaluated on real held-out series. A/B use their lane-specific gain and guard rules; a public benchmark comparison is not a substitute for that verdict. Two consequences:
- The model's learned signal comes only from subnet data — random init at
generation 0, and after a cascade promotion the init is itself the product of
earlier winning data — so diversity of regimes (trend, multiple
seasonalities, regime shifts, varied noise structure, realistic scales)
matters even more. A narrow or degenerate corpus teaches (or keeps) a narrow
forecaster, and a tiny one can't win by being memorised (the budget is
train_tokens, not a few epochs). From a warm-started init the marginal game is sharper still: data that covers what the promoted lineage is weak on beats data that repeats what it already does well. - Private test windows rotate each settlement; public benchmarks provide additional checks. Training data that covers varied, useful patterns is a stronger research approach than fitting only one visible benchmark. Neither a plausible-looking series nor a local benchmark win establishes a live win.
See scripts/example_generator/ for a runnable starting point.