Hyperparameter Optimization (HPO)¶
hpo
¶
In-node hyperparameter optimization — an Optuna study over the aligned backtest.
Gated on cfg.hpo.enabled; a strict no-op otherwise (a model with no search space, or HPO
off, tunes nothing and yields {} — exactly today's behavior). Two granularities, the DS-facing
knob in HpoConfig:
fleetwide(default): tune each model once on a representative sample of series and apply the winning params across all series — the only granularity affordable at 100k. Resolved on the driver (resolve_fleetwide) before the engine fans out, then threaded torun_cellas pre-resolvedparams(never viacfg— the config is the run_id identity key, so putting tuned params in it would shift the run_id and break reproducibility/idempotency; seemake_run_id).per_series: tune on each series insiderun_cell(heavier; a DS opt-in for the tail of hard series).
The objective reuses the aligned backtest: for one trial's params it runs
backtest_cell on each sampled series, averages the per-fold
panels, and scores on cfg.backtest.decision_metric. HPO therefore requires backtesting
(require_backtest) — there are no folds to tune on otherwise.
Pure + offline: no GCP, no Spark, deterministic (fixed-seed TPE sampler). The engines only add a tiny driver-side sample-and-resolve call in front of their existing fan-out.
require_backtest
¶
Raise if HPO is enabled without backtesting — the study has no folds to score on.
Source code in src/scale_forecasting/hpo.py
resolve_fleetwide
¶
Tune every model in cfg.models once on the shared sample → {model: params}.
The driver-side fleetwide pre-pass (the opinionated default): the winning params for each model
are applied across all series in the run. Models with no search space are simply absent from
the mapping (they keep their {} defaults in run_cell). Pure — the caller supplies the
pandas sample (a small, driver-collected set of series); this function does no I/O.
Source code in src/scale_forecasting/hpo.py
tune_model
¶
Tune one model on sample and return its winning params ({} if nothing to tune).
Builds a deterministic Optuna study (fixed-seed TPE) of cfg.hpo.n_trials trials whose
objective is _score_params. Returns {} immediately — creating no study — when the
model has no search space (_has_search_space), so an all-defaults model costs nothing.
cfg.model_params[model_name] sits underneath every trial. A model tuned without the
hyperparameters its author pinned is a different model from the one that will be fitted, so the
study would optimize the wrong thing — an authored n_lags changes what learning_rate is
best for. Where the two name the same key the trial wins, and the winner is returned with the
authored layer still beneath it, so the returned dict is exactly the params the cell will build
with and exactly what lands in forecast_metadata.best_params.
A model with no search space returns {} rather than the authored params: nothing was tuned,
and worker._resolve_params applies the authored layer at the cell either way.
tally counts the fits the search paid for, and only the per_series granularity passes
one. A search costs n_trials × sample × folds fits and none of them produce a shipped
forecast, so they are counted separately from the cell's own — worker.run_cell keeps two
tallies and writes them to n_hpo_fits and n_fits. The fleetwide pre-pass passes nothing:
it runs on the driver before any cell exists, so its fits belong to the run rather than to any
one row, and there is nowhere honest to put them.
Source code in src/scale_forecasting/hpo.py
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 | |