Forecast service¶
The forecast service (off-key-forecast) holds pretrained forecasting models for
forecaster steps: Mistral's Timestral checkpoints, and a seasonal-naive baseline
that needs no weights. A RADAR worker or a Dojo worker sends the recent values of
one reading and gets quantile forecasts back, so no model and no weights live in a
worker. Monitoring pipelines describes
the step itself.
How it is deployed¶
Production runs one forecast task on a backend node, from its own image. The
image carries CPU-only PyTorch and the timestral package (the service's
timestral extra, locked in backend/uv.lock); its build fails when they are not
installed or cannot compute. No other image carries them.
- The service has no published port and no route. It joins the app network only,
where workers reach it as
http://forecast:8020. - Every call but
/health/liveneeds a bearer token. The service, TACTIC, the Dojo workers and every RADAR worker read it from the Swarm secretFORECAST_API_TOKEN; it is never in a container's environment. TACTIC copies the secret reference to each RADAR worker it starts, from the same template service as the worker's MQTT credentials. - The browser never reaches the service. The composer asks the gateway, the gateway asks TACTIC, and TACTIC asks the service which checkpoints it holds and starts their downloads.
- TACTIC hands the address (
TACTIC_FORECAST_API_URL) to every RADAR worker and every preflight job asFORECAST_API_URL. A preflight job only checks that a service is named; it has no network and no token. The address is not part of a monitor's configuration fingerprint. - A release waits for the task's health check and then asks
/health/livefrom the app network (the readiness checkforecast endpoint). It never waits for weights.
The token is derived from internal_api_secret unless the environment vault sets
forecast_api_token (at least 32 characters). The derivation is a one-way hash,
so a worker that holds the token learns nothing about the internal credential.
Weights¶
Weights are not in the image. The service downloads a checkpoint from the Hugging
Face Hub into the forecast_models volume (/models) when it is asked for it:
by Download in the settings of a Timestral step, which lists every checkpoint
with its size and state, or by the first monitor or Dojo run that uses it. The
backend node therefore needs outbound HTTPS to huggingface.co and the hosts it
redirects downloads to. Values that arrive before the checkpoint is ready are
counted as lost coverage, not scored; the smallest checkpoint is ready within
seconds. Download needs the manage_monitors permission. The service
downloads one checkpoint at a time; the others wait their turn.
The volume is node-local and outlives redeployments, so a checkpoint downloads once. A task rescheduled to another backend node downloads what it is asked for again.
Each checkpoint is pinned to the Hub commit it was tested at
(forecast_timestral_checkpoints in the stack_prepare role defaults, rendered
into FORECAST_TIMESTRAL_MODELS as repo@commit). /v1/models and every
forecast report that commit as the model's revision. To move a checkpoint to a
newer commit, change its pin and release; the new weights download on first use
and the old ones stay in the volume until it is cleaned.
The service offers all four checkpoints, because the composer offers them and a step that names a checkpoint the service does not have ends its run. A checkpoint that nobody uses is never downloaded or loaded.
Sizing¶
Measured on a 4-core x86-64 machine with FORECAST_TORCH_THREADS=2, one series,
512 values of context, one step ahead (what a monitor asks with every value):
| Checkpoint | Download on a fast link | On disk | Forecast, median / 95th percentile |
|---|---|---|---|
mistralai/Timestral-5M |
3 s | 21 MB | 17 ms / 30 ms |
mistralai/Timestral-28M |
7 s | 111 MB | 59 ms / 67 ms |
mistralai/Timestral-123M |
11 s | 490 MB | 134 ms / 164 ms |
mistralai/Timestral-209M |
13 s | 837 MB | 212 ms / 231 ms |
Resident memory is about 85 MB before any model is loaded and grows as checkpoints are used: 0.3 GB with the smallest loaded, 0.4 GB with the two smallest, 0.9 GB with three and 1.7 GB with all four. Loading the largest checkpoint peaks at 2.5 GB for a moment. A loaded checkpoint stays loaded until the task restarts. A full batch of long series (16 series of 4,096 values, 96 steps ahead) did not raise that peak.
The defaults follow from this: a 4 GB memory limit and 2 CPUs
(forecast_memory_limit, forecast_cpu_limit), with 512 MB and a quarter CPU
reserved (forecast_memory_reservation, forecast_cpu_reservation). All
forecasts share those CPUs: a monitor that gets one value per second and uses the
largest checkpoint keeps the service busy about a fifth of the time, so many
monitors on the larger checkpoints need more CPUs or the smaller checkpoints.
Settings¶
| Ansible variable | Default | Becomes |
|---|---|---|
forecast_api_url |
http://forecast:8020 |
TACTIC_FORECAST_API_URL for TACTIC, FORECAST_API_URL for workers |
forecast_api_token (vault, optional) |
derived from internal_api_secret |
Swarm secret FORECAST_API_TOKEN |
forecast_timestral_checkpoints |
the four checkpoints, each with its commit | FORECAST_TIMESTRAL_MODELS |
forecast_torch_threads |
2 |
FORECAST_TORCH_THREADS |
forecast_memory_limit, forecast_cpu_limit |
4G, 2 |
the task's limits |
forecast_memory_reservation, forecast_cpu_reservation |
512M, 0.25 |
the task's reservations |
Environment variables lists what the service and the workers read.
When something is wrong¶
| What you see | What it means |
|---|---|
Preflight rejects a Timestral step with forecast_service |
The worker's environment names no service: TACTIC_FORECAST_API_URL is empty. |
| A monitor's coverage drops while it keeps running | The service did not answer, or the checkpoint is still downloading. Scores resume by themselves; the model's series has no gap. |
| A run ends right after it starts | The step names a checkpoint that FORECAST_TIMESTRAL_MODELS does not offer. |
| The composer shows the Timestral step greyed out, with "This deployment has no forecast service" | TACTIC is told no service: TACTIC_FORECAST_API_URL is empty. |
| A Timestral step's settings say the forecast service does not answer | TACTIC could not reach the service within 3 seconds, or the service refused its token. Read the forecast task's state and TACTIC's log line tactic.forecast_models_unreachable. |
A checkpoint is listed as Download failed, or /v1/models shows it as failed |
Its download failed; the reason is listed with it (usually no route to the Hub). Try again, or the next use, asks again. |
The release fails on forecast endpoint |
The task is not running or not reachable on the app network; read its task errors and logs. |
The settings of a Timestral step show what the service holds. To ask the service itself, do it from a Dojo worker, which holds the token. On the node that runs one:
docker exec "$(docker ps -q -f name=off-key_dojo-worker | head -1)" /app/bin/python -c \
"from off_key_mqtt_radar.forecasting import service_client; [print(m.id, m.status, m.revision) for m in service_client().models()]"
Local development¶
The local stacks leave the service out, because its image is large. Start it with
the forecast Compose profile and give the workers its address:
With plain Compose, set FORECAST_API_URL=http://forecast:8020 in .env and run
docker compose --profile forecast up -d --build. A development machine uses no
token.
Licence¶
The Timestral code is Apache 2.0. The weights are under Mistral AI's Modified MIT licence; they are downloaded from Mistral's repositories at run time and are not redistributed with this project.