cascade-broad-v1 — wall-saturating, cluster-broad candidate generator
cascade SN91 submission repo. Three files are the contract
(generator.py, config.json, requirements.txt); this README is documentation.
/tmp/.../venv/bin/cascade verify /workspace/cascade-rig/gen/candidate
/tmp/.../venv/bin/python /workspace/cascade-rig/rig/bench_throughput.py \
/workspace/cascade-rig/gen/candidate --seconds 20
Measured (this box, single core, seed 9993167447787770989)
| metric | value |
|---|---|
cascade verify |
OK — [deterministic], corpus_digest (seed 0) 4fdbf700a0b32b0c… |
| emitted throughput | 13,468,137 points/s = 364.0 % of the 3,700,000 pts/s wall (two 20 s runs: 13.47 M / 13.91 M, i.e. 364–376 %) |
| series rate | 5,479 series/s (saturation needs 1,712 at this mean length) |
| mean length | 2,161 — 5 distinct lengths, all multiples of 32 |
| 32-align waste | 0.00 % (every emitted point is trained on) |
tokens_frac heat / final |
1.000 / 1.000 |
| first-series latency | 0.06 s (per-frame stall timeout is 1800 s) |
peak memory under RLIMIT_AS = 4 GiB |
VmPeak 906 MB, RSS 162 MB — 120k series streamed clean |
| exact byte-duplicate series | 0.007 % (cap is 5 %) |
non-finite after the trainer's causal_standardize + arcsinh |
0 / 43.2 M tokens, max |z| = 4.85 |
Why it is fast
generate() never touches a single series in Python. It draws a block of B
series of one identical, 32-aligned length L as one (B, L) float32 array,
entirely with vectorised numpy plus scipy.signal.lfilter for the recursive
parts, then yields the rows one at a time. Every recursion is a filter along
the time axis of the whole batch (C-level), never a Python loop over time or
over series.
points_per_block = 1,048,576⇒B = 1M / L(256 rows atL=4096, 4096 rows atL=256), rounded to a multiple of 64 = the trainer'sbatch_size, sop-buckets flush without long-lived partials.- float32 blocks.
check_seriesaccepts any floating dtype and the trainer canonicalises to float64 losslessly. float32 halves memory traffic and takes the SIMD math path:cosis 2.5 ns/pt vs 21.7 ns/pt in float64 on this box. interleave_slots = 6. Rows are drawn round-robin from 6 concurrently-open blocks, so a trainer minibatch is a mixture of families instead of 64 rows cut from one block. Costs ~nothing; removes the intra-batch correlation that block-sequential emission would otherwise hand the optimiser.- Per-row gates. Optional components (trend, level shift, bump, outage,
stuck sensor, negative-price regime, …) are switched by a
(B,1)0/1 mask folded into the amplitude, not by anifaround the whole block — full per-series diversity at the cost of one multiply. - Cheap smooth components.
_slow()draws control knots atL/kresolution and linearly interpolates instead of filtering white noise at full rate (~10× cheaper for the same low-frequency content).
Family cost, measured at B=512, L=2048 (ns per emitted point → equivalent
Mpts/s if that family ran alone):
| family | ns/pt | Mpts/s alone |
|---|---|---|
power_load |
44 | 22.6 |
fin_walk |
48 | 20.7 |
step_plateau |
55 | 18.3 |
hydro_burst |
59 | 17.0 |
geo_smooth |
70 | 14.3 |
dock_counts |
74 | 13.5 |
kernel_mix |
94 | 10.6 |
daily_business |
102 | 9.8 |
energy_price |
127 | 7.9 |
degenerate |
30 | 33.2 |
| weighted mixture | 77 | 13.0 |
Dominant remaining costs are rng.standard_normal (≈16 ns/pt even in float32)
and the float64 round-trip that lfilter needs. If more headroom is ever wanted:
switch the AR innovations of energy_price/kernel_mix from gaussian to the
uniform innovation path (_innov kind 1, 4.3 ns/pt) — but there is 3.8× of slack
today, and past the wall further speed is worth exactly zero.
Length choice
lengths = [4096, 2048, 1024, 512, 256] — all multiples of 32, so the trainer's
p = min(L//32, 128) slice s[-p*32:] consumes 100 % of every emitted point
(measured 32-align waste 0.00 %). Only 5 distinct lengths ⇒ only 5 p-buckets,
each filling to 64 rows fast.
length_weights are the fraction of series at each length (the generator
divides by that length's rows-per-block to get the block-draw probability).
Defaults [0.30, 0.26, 0.22, 0.14, 0.08] put 63 % of emitted points at
L = 4096, the exact context width the evaluator feeds, while keeping shorter
contexts in the mix. Per-family overrides skew minute-cadence families long and
daily families short, matching how long those feeds actually are.
The mixture — families, weights, target clusters
A family's weight is its block-draw probability, which equals its share of
emitted points (every block is ~1 M points regardless of family/length), i.e.
its share of the training token budget and therefore of the loss. Measured point
share over 200k series is within sampling noise of the configured weight.
Pool facts these weights are aimed at (pool/metadata_0726.json, 1746 series,
214 source clusters):
- cluster share (what the duel LCB counts): energy 33.2 %, econ_fin 19.6 %, nature 17.3 %, healthcare 10.7 %, transport 7.9 %, web_cloudops 7.5 %, sales 3.7 %
- cluster share by cadence:
15min/sp 96 → 29 %,D/sp 7 → 28.5 %,min/sp 60 → 17.8 %,H/sp 24 → 13 %,5min/sp 288 → 5 %,30min/sp 48 → 3.3 % - window share (what the heat ranks): transport 68.3 %, of which GBFS bike-share is 57 % of all windows from just 9 clusters
So the mixture has to serve two different objectives at once: heat = minute-cadence bounded counts, duel = breadth over energy / econ_fin / nature.
| family | weight | target cluster group (live pool) | what it emits | key params |
|---|---|---|---|---|
dock_counts |
0.16 | transport — 17 GBFS *_station_status clusters (57 % of heat windows), tfl_arrivals; sp 60, minute cadence |
Bounded integer counts. Smooth latent (diurnal + 2nd harmonic + hourly rhythm + slow drift + sparse rebalancing steps) rounded to integers and clipped to [0, cap]. Gives exactly the real shape: long exact plateaus, ±1 transitions (measured 90 % of all non-zero steps are exactly ±1), 12–15 % exact zeros, ac(1) 0.99, dead-station runs pinned at 0 or cap. No affine rescale — integers stay integers |
cap_lo/hi 5–64, day_periods [1440,720,480,2880,240], hour_period 60, rebalance_rate 0.0016, dead_prob 0.04 |
kernel_mix |
0.16 | general prior / web_cloudops / anything unmodelled |
3 random components from {grouped-AR(1) with r∈[0.30,0.995] over gaussian, uniform or heavy-tailed innovations; interpolated smooth GP; harmonic pair} + per-row trend and level shift. The stationary-kernel-mixture backbone |
n_components 3, ar_r_lo/hi, slow_cells, periods [7…336], innov_weights |
energy_price |
0.12 | energy price feeds (~30 of 71 energy clusters): *_day_ahead_price, nordpool, caiso_lmp_5min, octopus_agile, eia_gas_spot; sp 96/48/24/288 |
Positive base level + daily + half-daily + weekly harmonics, heavy-tailed AR noise, one-sided scarcity spikes with exponential decay, per-row negative-price regime, per-row regulated cap/floor. Measured kurtosis 60, 23 % negative prints | periods+period_weights (96 weighted 0.42), spike_rate 0.0025, spike_mag 2–25, neg_prob 0.35, cap_prob 0.30 |
daily_business |
0.12 | D/sp 7 clusters — 61 of 214: rki_covid_hospitalizations, npm_downloads, wikimedia_pageviews, crw_sst, apnic_ipv6, daily econ_fin |
Weekly harmonics plus a hard non-sinusoidal weekend notch, trend or logistic adoption S-curve, sparse level shifts, gaussian bump (outbreak/campaign), holiday dips, multiplicative log-normal noise, optional integer counts. Measured ac(lag 7) = 0.90 | period 7, annual_period 365, logistic_prob 0.20, bump_prob 0.35, holiday_rate 0.008, count_prob 0.45 |
power_load |
0.10 | energy non-price (~35 clusters): *_public_power, *_co2_intensity, eia_hourly_demand, energinet_power_system, cross_border_flows |
Smooth positive load/generation: solar bell clipped at zero overnight (max(0,sin)^e) or double-peak demand, weekly modulation, slow weather drift, small noise, per-row curtailment outage to exact zero. ac(1) 0.98, ac(sp) 0.76 |
solar_prob 0.40, solar_exp 1.0–2.2, weather_cells 12, outage_prob 0.10 |
geo_smooth |
0.09 | nature (37 clusters): metar_us_airports (sp 288), datalakes_eawag (144), noaa_tides_coops (240), newcastle_urban_obs (48), usgs_streamflow (96) |
Strong diurnal + slow seasonal drift + tiny AR noise; a tide mode sums 4 incommensurate harmonics (M2/S2/K1/O1 ratios) so the envelope beats; per-row frozen-sensor runs. ac(1) 0.996 — the smoothest family | periods+weights, tide_prob 0.30, beat_ratios, ar_r 0.90–0.999, stuck_prob 0.06 |
fin_walk |
0.08 | econ_fin (42 clusters): coinbase_spot_1m/15m, FX, yields, spreads |
Stochastic-volatility random walk: interpolated log-vol × heavy-tailed innovations, cumsum or mean-reverting, geometric (positive price) or additive (yield/spread), optional tick-grid quantisation → microstructure plateaus. ac(1) 0.998 (near unit root) | vol_cells 32, base_vol 8e-4–2e-2, geometric_prob 0.55, tick_prob 0.35, meanrev_prob 0.25 |
hydro_burst |
0.06 | nature bursty: ea_uk_rainfall_15min, usgs_streamflow_iv; doubles as intermittent-demand shape |
Rain mode = zero-inflated clustered bursts (mostly exact zeros, exponential magnitudes); flow mode = sparse impulses through a slow recession filter (r∈[0.90,0.9985]) → fast rise / slow decay asymmetry, plus baseflow. Measured kurtosis 72 |
rain_prob 0.45, recession_lo/hi, event_rate 8e-4–2e-2, mag_scale 1–12 |
degenerate |
0.06 | the log-floor cliff, every domain | 6 modes: flat (continuous level, one micro-blip), flat_step, intermittent (exact zeros + rare spikes), collapse (live → frozen tail), frozen_head (frozen → live), outlier (flat + one extreme). Measured longest flat run 74 % of L, kurtosis 3950, ptp/sd 64 |
mode_weights (6 keys), cut_frac 0.15–0.85, spike_rate 0.002–0.08 |
step_plateau |
0.05 | econ_fin policy rates / tariffs, energy set-points, thermostats |
Sparse jumps → cumsum → grid quantisation = long exactly-constant plateaus with rare jumps; or a duty-cycle square wave. Per-row slow ramp and per-row observation noise. 3.7 % of rows are exactly constant end-to-end | jump_rate 3e-4–6e-3, grid 0.05–1.0, square_prob 0.25, duty 0.15–0.85 |
Coverage check against the eight shapes the brief asked for: bounded integer
counts → dock_counts; degenerate/frozen/intermittent → degenerate (+ the pad
family, + power_load outages, + geo_smooth stuck sensors); daily-seasonal
business with trend and level shifts → daily_business; 15min/5min electricity
with heavy tails, negatives and spikes → energy_price; smooth geophysical →
geo_smooth; step functions with long plateaus → step_plateau; GP/kernel
mixture and OU → kernel_mix; plus power_load, hydro_burst and fin_walk
for the rest of the energy / nature / econ_fin cluster mass.
The pad family (eval-geometry match)
cascade's evaluator left-pads every history with h[0] out to the full 4096
before the forward pass (toto2_trainer.py:596-613); training otherwise never
sees that shape, and causal_standardize pins scale = 1e-5 on a flat context —
a log-space cliff.
pad in config.json is applied orthogonally to every family after the
affine step: a fraction of rows get their first R samples overwritten with
s[R], i.e. a genuine constant head equal to the first live value.
"pad": { "fraction": 0.12, "run_frac_lo": 0.05, "run_frac_hi": 0.80, "run_frac_beta": 1.6 }
run_frac_beta > 1 skews R/L short, < 1 skews it long; fraction is the
sweep knob. Measured: 16.3 % of corpus series carry a flat head ≥ 5 % of L
(pad + naturally-frozen families), and 7.9 % of all standardized steps sit on
the eps scale floor — the model trains on the cliff instead of meeting it
first at eval. Verified end to end: an eval-style pad (512 real samples inflated
to 4096) goes through causal_standardize finite, max |z| 4.8.
Safety rails
One bad series aborts the whole round (sandbox_stream_failed), so _finalize
runs on every block before any row is yielded:
np.nan_to_num(..., copy=False)— NaN → 0, ±inf → ±max_abs_value.np.clip(±max_abs_value)— default 1e9, far underCAST_SAFE_MAX_FLOAT32(3.4e38); measured clip rate 0.000 % on every family, so the rail never distorts the prior.finite_check— onenp.isfinitepass, any survivor zeroed.- Lengths are drawn from a fixed 32-aligned table inside
[64, 4096]and validated in__init__(raises if a config edit breaks the band). - Flat series always carry a continuous level, so two of them can never be byte-identical — the corpus dup cap is 5 % and measured duplicates are 0.007 %.
- A malformed
config.jsonis swallowed and the built-inDEFAULTSare used; an unknown family name in config is ignored rather than fatal.
Determinism
np.random.default_rng(seed)only, one generator, drawn in a fixed order.np.random.seedappears nowhere — production'sgeneration_seedis a full 64-bit blake2b value (e.g. 9993167447787770989) andnp.random.seedrejects anything ≥ 2³², which would passcascade verify(seed 0) and die in the round.- Verified:
cascade verifyprints[deterministic]; twoGenerators at 9993167447787770989 produce identical arrays, and seed±1 produces different ones. - Grouped
lfilteriterates group ids0..n-1in fixed order; no reduction-order-dependent parallelism, no threads, no wall-clock, noos.urandom. generate(n)depends only on(seed, n)and its firstkoutputs are independent ofn— with the interleave pool, blocks are still built in a fixed rng-driven order.
Sweeping
Everything in the table above is in config.json; nothing needs a code edit:
- mixture:
families.<name>.weight(= point/token share),length_weights - eval geometry:
pad.fraction,pad.run_frac_lo/hi/beta - magnitudes:
affine.log10_scale_lo/hi,offset_sigma,sign_flip_prob - throughput/quality trade:
points_per_block,interleave_slots,ar_groups - every per-family parameter listed in the table
Note the ratchet: config-only forks are kept today (dedup_config_only_enforce=false)
but that is the named next change, so the code is written to make real edits
cheap too — a new family is one _fam_*(rng, B, L, p) function returning a
(B, L) float32 array plus one entry in _FAMILY_FN and one block in DEFAULTS.
Dependencies
generator.py imports only numpy and scipy.signal.lfilter (plus stdlib
json/math/pathlib), both allowlisted in chain.toml [dependencies] and
both baked into deploy/Dockerfile (numpy 2.4.6, scipy 1.17.1) — remember
requirements.txt is never installed server-side. No numba is needed: the
mixture already runs at 3.8× the wall on pure numpy + scipy. Nothing on the
static-guard blocklist is imported, and no weight file of any kind ships.